Вичерпний посібник з потужних зіставних та умовних типів у TypeScript, що включає практичні приклади та розширені сценарії використання для створення надійних і типобезпечних застосунків.
Опанування зіставних (Mapped Types) та умовних (Conditional Types) типів у TypeScript
TypeScript, що є надмножиною JavaScript, пропонує потужні можливості для створення надійних та підтримуваних застосунків. Серед цих можливостей зіставні типи (Mapped Types) та умовні типи (Conditional Types) виділяються як основні інструменти для розширеного маніпулювання типами. Цей посібник надає вичерпний огляд цих концепцій, досліджуючи їх синтаксис, практичне застосування та розширені сценарії використання. Незалежно від того, чи ви досвідчений розробник на TypeScript, чи тільки починаєте свій шлях, ця стаття надасть вам знання для ефективного використання цих функцій.
Що таке зіставні типи (Mapped Types)?
Зіставні типи дозволяють створювати нові типи шляхом перетворення існуючих. Вони ітеруються по властивостях існуючого типу та застосовують перетворення до кожної властивості. Це особливо корисно для створення варіацій існуючих типів, наприклад, роблячи всі властивості необов'язковими або доступними лише для читання.
Базовий синтаксис
Синтаксис зіставного типу виглядає наступним чином:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: Вхідний тип, який ви хочете зіставити.K in keyof T
: Ітерує по кожному ключу у вхідному типіT
.keyof T
створює об'єднання всіх імен властивостей уT
, аK
представляє кожен окремий ключ під час ітерації.Transformation
: Перетворення, яке ви хочете застосувати до кожної властивості. Це може бути додавання модифікатора (наприклад,readonly
або?
), зміна типу або щось інше.
Практичні приклади
Створення властивостей лише для читання
Припустимо, у вас є інтерфейс, що представляє профіль користувача:
interface UserProfile {
name: string;
age: number;
email: string;
}
Ви можете створити новий тип, де всі властивості доступні лише для читання:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Тепер ReadOnlyUserProfile
матиме ті ж властивості, що й UserProfile
, але всі вони будуть доступні лише для читання.
Створення необов'язкових властивостей
Аналогічно, ви можете зробити всі властивості необов'язковими:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
матиме всі властивості UserProfile
, але кожна з них буде необов'язковою.
Зміна типів властивостей
Ви також можете змінювати тип кожної властивості. Наприклад, можна перетворити всі властивості на рядки:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
У цьому випадку всі властивості в StringifiedUserProfile
матимуть тип string
.
Що таке умовні типи (Conditional Types)?
Умовні типи дозволяють визначати типи, що залежать від певної умови. Вони надають спосіб вираження відношень між типами на основі того, чи задовольняє тип певне обмеження. Це схоже на тернарний оператор у JavaScript, але для типів.
Базовий синтаксис
Синтаксис умовного типу виглядає наступним чином:
T extends U ? X : Y
T
: Тип, що перевіряється.U
: Тип, який розширюєT
(умова).X
: Тип, що повертається, якщоT
розширюєU
(умова істинна).Y
: Тип, що повертається, якщоT
не розширюєU
(умова хибна).
Практичні приклади
Визначення, чи є тип рядком
Створімо тип, який повертає string
, якщо вхідний тип є рядком, і number
в іншому випадку:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Видобування типу з об'єднання (Union)
Ви можете використовувати умовні типи для видобування конкретного типу з об'єднання типів. Наприклад, для вилучення типів, що не можуть бути null:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Тут, якщо T
є null
або undefined
, тип стає never
, який потім відфільтровується спрощенням об'єднаних типів у TypeScript.
Виведення типів (Inferring)
Умовні типи також можна використовувати для виведення типів за допомогою ключового слова infer
. Це дозволяє витягувати тип із більш складної структури типів.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
У цьому прикладі ReturnType
витягує тип, що повертається функцією. Він перевіряє, чи є T
функцією, що приймає будь-які аргументи і повертає тип R
. Якщо так, він повертає R
; інакше повертає any
.
Поєднання зіставних та умовних типів
Справжня сила зіставних та умовних типів проявляється при їх поєднанні. Це дозволяє створювати надзвичайно гнучкі та виразні перетворення типів.
Приклад: Глибокий Readonly (Deep Readonly)
Поширеним випадком є створення типу, який робить всі властивості об'єкта, включаючи вкладені, доступними лише для читання. Цього можна досягти за допомогою рекурсивного умовного типу.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
Тут DeepReadonly
рекурсивно застосовує модифікатор readonly
до всіх властивостей та їхніх вкладених властивостей. Якщо властивість є об'єктом, він рекурсивно викликає DeepReadonly
для цього об'єкта. В іншому випадку, він просто застосовує модифікатор readonly
до властивості.
Приклад: Фільтрація властивостей за типом
Припустимо, ви хочете створити тип, який включає лише властивості певного типу. Ви можете поєднати зіставні та умовні типи для досягнення цього.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
У цьому прикладі FilterByType
ітерується по властивостях T
і перевіряє, чи розширює тип кожної властивості тип U
. Якщо так, він включає властивість до результуючого типу; інакше, він виключає її, зіставляючи ключ із never
. Зверніть увагу на використання "as" для перезіставлення ключів. Потім ми використовуємо `Omit` та `keyof StringProperties` для видалення рядкових властивостей з оригінального інтерфейсу.
Розширені сценарії використання та патерни
Окрім базових прикладів, зіставні та умовні типи можна використовувати в більш просунутих сценаріях для створення висококастомізованих і типобезпечних застосунків.
Дистрибутивні умовні типи
Умовні типи є дистрибутивними, коли тип, що перевіряється, є об'єднанням типів. Це означає, що умова застосовується до кожного члена об'єднання окремо, а результати потім об'єднуються в новий тип об'єднання.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
У цьому прикладі ToArray
застосовується до кожного члена об'єднання string | number
окремо, що призводить до string[] | number[]
. Якби умова не була дистрибутивною, результатом було б (string | number)[]
.
Використання утилітних типів
TypeScript надає кілька вбудованих утилітних типів, які використовують зіставні та умовні типи. Ці утилітні типи можна використовувати як будівельні блоки для більш складних перетворень типів.
Partial<T>
: Робить усі властивостіT
необов'язковими.Required<T>
: Робить усі властивостіT
обов'язковими.Readonly<T>
: Робить усі властивостіT
доступними лише для читання.Pick<T, K>
: Вибирає набір властивостейK
зT
.Omit<T, K>
: Видаляє набір властивостейK
зT
.Record<K, T>
: Створює тип з набором властивостейK
типуT
.Exclude<T, U>
: Виключає зT
усі типи, які можна призначитиU
.Extract<T, U>
: Витягує зT
усі типи, які можна призначитиU
.NonNullable<T>
: Виключаєnull
таundefined
зT
.Parameters<T>
: Отримує параметри функціонального типуT
.ReturnType<T>
: Отримує тип повернення функціонального типуT
.InstanceType<T>
: Отримує тип екземпляра конструктора функціонального типуT
.
Ці утилітні типи є потужними інструментами, які можуть спростити складні маніпуляції з типами. Наприклад, ви можете поєднати Pick
та Partial
, щоб створити тип, який робить необов'язковими лише певні властивості:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
У цьому прикладі OptionalDescriptionProduct
має всі властивості Product
, але властивість description
є необов'язковою.
Використання шаблонних літеральних типів
Шаблонні літеральні типи дозволяють створювати типи на основі рядкових літералів. Їх можна використовувати в поєднанні з зіставними та умовними типами для створення динамічних та виразних перетворень типів. Наприклад, ви можете створити тип, який додає префікс до всіх імен властивостей:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
У цьому прикладі PrefixedSettings
матиме властивості data_apiUrl
та data_timeout
.
Найкращі практики та рекомендації
- Зберігайте простоту: Хоча зіставні та умовні типи є потужними, вони також можуть ускладнити ваш код. Намагайтеся робити перетворення типів якомога простішими.
- Використовуйте утилітні типи: Використовуйте вбудовані утилітні типи TypeScript, коли це можливо. Вони добре протестовані та можуть спростити ваш код.
- Документуйте свої типи: Чітко документуйте перетворення типів, особливо якщо вони складні. Це допоможе іншим розробникам зрозуміти ваш код.
- Тестуйте свої типи: Використовуйте перевірку типів TypeScript, щоб переконатися, що ваші перетворення типів працюють як очікувалося. Ви можете писати юніт-тести для перевірки поведінки ваших типів.
- Враховуйте продуктивність: Складні перетворення типів можуть впливати на продуктивність компілятора TypeScript. Пам'ятайте про складність ваших типів і уникайте непотрібних обчислень.
Висновок
Зіставні типи та умовні типи — це потужні функції TypeScript, які дозволяють створювати надзвичайно гнучкі та виразні перетворення типів. Опанувавши ці концепції, ви можете покращити типобезпечність, підтримуваність та загальну якість ваших застосунків на TypeScript. Від простих перетворень, таких як зроблення властивостей необов'язковими або доступними лише для читання, до складних рекурсивних перетворень та умовної логіки, ці функції надають інструменти, необхідні для створення надійних та масштабованих застосунків. Продовжуйте досліджувати та експериментувати з цими функціями, щоб розкрити їх повний потенціал і стати більш досвідченим розробником TypeScript.
Продовжуючи свою подорож у світ TypeScript, не забувайте використовувати багатство доступних ресурсів, включаючи офіційну документацію TypeScript, онлайн-спільноти та проєкти з відкритим кодом. Використовуйте потужність зіставних та умовних типів, і ви будете добре підготовлені до вирішення навіть найскладніших проблем, пов'язаних з типами.