TypeScript - Мapped Types: Пособие для начинающих

Здравствуйте, будущие маги TypeScript! Сегодня мы отправимся в увлекательное путешествие в мир Mapped Types. Не беспокойтесь, если вы новички в программировании - я буду вашим дружелюбным проводником, и мы будем двигаться шаг за шагом. К концу этого руководства вы будете mapping типы как профессионал!

TypeScript - Mapped Types

Что такое Mapped Types?

Прежде чем мы углубимся, давайте поймем, что такое Mapped Types. Представьте, что у вас есть коробка с разнообразными конфетами, и вы хотите создать новую коробку, где каждая конфета завернута в золотую фольгу. Именно это и делают Mapped Types в TypeScript - они берут существующий тип и преобразуют его в новый тип на основе определенного набора правил.

Встроенные Mapped Types

TypeScript предоставляет некоторые预先 определенные Mapped Types, которые都非常 полезны. Давайте рассмотрим их по одному:

1. Partial

Тип Partial<T> делает все свойства T可选. Это как转变 строгого рецепта в гибкий, где вы можете пропустить некоторые ингредиенты.

interface Recipe {
name: string;
ingredients: string[];
cookingTime: number;
}

type FlexibleRecipe = Partial<Recipe>;

// Теперь мы можем создать рецепт без всех свойств
const quickSnack: FlexibleRecipe = {
name: "Toast",
// Мы можем пропустить ingredients и cookingTime
};

В этом примере FlexibleRecipe позволяет нам создать рецепт без указания всех свойств. Это идеально, когда вы хотите обновить только часть объекта.

2. Required

Required<T> - это相反 Partial<T>. Он делает все свойства обязательными, даже если они были可选 в исходном типе.

interface UserProfile {
name: string;
age?: number;
email?: string;
}

type CompleteUserProfile = Required<UserProfile>;

// Теперь мы должны указать все свойства
const user: CompleteUserProfile = {
name: "Alice",
age: 30,
email: "[email protected]"
};

Здесь CompleteUserProfile обеспечивает указание всех свойств, включая age и email, которые были可选 в исходном UserProfile.

3. Readonly

Readonly<T> делает все свойства T только для чтения. Это как поместить ваш тип в стеклянный футляр - вы можете смотреть, но не можете трогать!

interface Toy {
name: string;
price: number;
}

type CollectibleToy = Readonly<Toy>;

const actionFigure: CollectibleToy = {
name: "Superhero",
price: 19.99
};

// Это вызовет ошибку
// actionFigure.price = 29.99;

В этом примере,.once мы создали наш actionFigure, мы не можем изменять его свойства. Это великолепно для создания неизменяемых объектов.

4. Pick<T, K>

Pick<T, K> создает новый тип, выбирая только указанные свойства K из T. Это как cherry-picking ваши любимые функции из типа.

interface Smartphone {
brand: string;
model: string;
year: number;
color: string;
price: number;
}

type BasicPhoneInfo = Pick<Smartphone, 'brand' | 'model'>;

const myPhone: BasicPhoneInfo = {
brand: "TechBrand",
model: "X2000"
};

Здесь BasicPhoneInfo включает только свойства brand и model из Smartphone, оставляя其余ые.

5. Omit<T, K>

Omit<T, K> - это相反 Pick<T, K>. Он создает новый тип, удаляя указанные свойства K из T.

interface Book {
title: string;
author: string;
pages: number;
isbn: string;
}

type BookPreview = Omit<Book, 'pages' | 'isbn'>;

const preview: BookPreview = {
title: "TypeScript Adventures",
author: "Code Wizard"
};

В этом случае BookPreview включает все свойства Book, кроме pages и isbn.

Примеры использования Mapped Types

Теперь, когда мы рассмотрели встроенные Mapped Types, давайте посмотрим на некоторые практические примеры их использования в реальных сценариях.

Пример 1: Создание состояния формы

Представьте, что вы создаете форму, и хотите отслеживать, какие поля были изменены:

interface LoginForm {
username: string;
password: string;
rememberMe: boolean;
}

type FormTouched = { [K in keyof LoginForm]: boolean };

const touchedFields: FormTouched = {
username: true,
password: false,
rememberMe: true
};

Здесь мы создали тип FormTouched, который имеет те же ключи, что и LoginForm, но все значения являются булевыми, указывающими, было ли поле touched.

Пример 2: Обертка для ответа API

Давайте представим, что у вас есть API, который возвращает различные типы данных, и вы хотите обернуть каждый ответ в стандартный формат:

interface ApiResponse<T> {
data: T;
status: 'success' | 'error';
timestamp: number;
}

type UserData = { id: number; name: string; email: string };
type ProductData = { id: number; name: string; price: number };

const userResponse: ApiResponse<UserData> = {
data: { id: 1, name: "John Doe", email: "[email protected]" },
status: 'success',
timestamp: Date.now()
};

const productResponse: ApiResponse<ProductData> = {
data: { id: 101, name: "Laptop", price: 999.99 },
status: 'success',
timestamp: Date.now()
};

Этот пример показывает, как мы можем использовать обобщения с Mapped Types для создания гибких, повторно используемых структур типов.

Создание пользовательских Mapped Types

Теперь давайте разомнем наши мышцы TypeScript и создадим некоторые пользовательские Mapped Types!

Пользовательский тип 1: Nullable

Давайте создадим тип, который делает все свойства nullable:

type Nullable<T> = { [K in keyof T]: T[K] | null };

interface Person {
name: string;
age: number;
}

type NullablePerson = Nullable<Person>;

const maybePerson: NullablePerson = {
name: "Jane",
age: null  // Это теперь допустимо
};

Наш тип Nullable<T> позволяет любому свойству быть либо его исходным типом, либо null.

Пользовательский тип 2: Freezable

Давайте создадим тип, который добавляет метод freeze к объекту:

type Freezable<T> = T & { freeze(): Readonly<T> };

interface Config {
theme: string;
fontSize: number;
}

function makeFreezable<T>(obj: T): Freezable<T> {
return {
...obj,
freeze() {
return Object.freeze({ ...this }) as Readonly<T>;
}
};
}

const config = makeFreezable<Config>({
theme: "dark",
fontSize: 14
});

const frozenConfig = config.freeze();
// frozenConfig.theme = "light";  // Это вызовет ошибку

Этот пользовательский тип добавляет метод freeze к любому объекту, позволяя нам создать неизменяемую версию его.

Заключение

Ух, мы covered много ground сегодня! От встроенных Mapped Types до создания своих собственных пользовательских типов, вы видели, насколько мощным и гибким может быть TypeScript. Mapped Types - это как магические палочки в вашем наборе инструментов TypeScript - они позволяют вам преобразовывать и манипулировать типами самыми полезными способами.

Помните, ключ к maîtriser Mapped Types - это практика. Попробуйте создавать свои собственные, экспериментируйте с различными комбинациями, и вскоре вы будете писать код на TypeScript, который не только функционален, но и элегантен и типобезопасен.

Продолжайте программировать, продолжайте учиться и, самое главное, получайте удовольствие от работы с TypeScript!

Credits: Image by storyset