Изчерпателно ръководство за мощните Mapped Types и Conditional Types в TypeScript, с практически примери и разширени случаи на употреба за създаване на надеждни и типово-безопасни приложения.
Овладяване на Mapped Types и Conditional Types в TypeScript
TypeScript, надмножество на JavaScript, предлага мощни функции за създаване на стабилни и лесни за поддръжка приложения. Сред тези функции, Mapped Types и Conditional Types се открояват като основни инструменти за напреднала манипулация на типове. Това ръководство предоставя изчерпателен преглед на тези концепции, изследвайки техния синтаксис, практически приложения и разширени случаи на употреба. Независимо дали сте опитен TypeScript разработчик или тепърва започвате, тази статия ще ви даде знанията, за да използвате тези функции ефективно.
Какво представляват Mapped Types?
Mapped Types ви позволяват да създавате нови типове, като трансформирате съществуващи. Те итерират върху свойствата на съществуващ тип и прилагат трансформация на всяко свойство. Това е особено полезно за създаване на вариации на съществуващи типове, като например да направите всички свойства незадължителни или само за четене.
Основен синтаксис
Синтаксисът на Mapped Type е следният:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: Входният тип, върху който искате да приложите трансформацията.K in keyof T
: Итерира върху всеки ключ във входния типT
.keyof T
създава обединение (union) на всички имена на свойства в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?
Conditional Types ви позволяват да дефинирате типове, които зависят от условие. Те предоставят начин за изразяване на типови зависимости въз основа на това дали даден тип удовлетворява определено ограничение. Това е подобно на тернарния оператор в JavaScript, но за типове.
Основен синтаксис
Синтаксисът на Conditional Type е следният:
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)
Можете да използвате условни типове, за да извлечете конкретен тип от обединение от типове (union type). Например, за да извлечете типове, които не могат да бъдат null:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Тук, ако T
е null
или undefined
, типът става never
, който след това се филтрира от опростяването на обединенията в TypeScript.
Извличане на типове (Inferring Types)
Условните типове могат да се използват и за извличане на типове с помощта на ключовата дума 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
.
Комбиниране на Mapped Types и Conditional Types
Истинската сила на Mapped Types и Conditional Types идва от тяхното комбиниране. Това ви позволява да създавате изключително гъвкави и експресивни трансформации на типове.
Пример: 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
към свойството.
Пример: Филтриране на свойства по тип
Да приемем, че искате да създадете тип, който включва само свойства от определен тип. Можете да комбинирате Mapped Types и Conditional Types, за да постигнете това.
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`, за да премахнем свойствата от тип низ от оригиналния интерфейс.
Разширени случаи на употреба и модели
Освен основните примери, Mapped Types и Conditional Types могат да се използват в по-напреднали сценарии за създаване на силно персонализирани и типово-безопасни приложения.
Дистрибутивни условни типове
Условните типове са дистрибутивни, когато проверяваният тип е обединение (union type). Това означава, че условието се прилага към всеки член на обединението поотделно, а резултатите след това се комбинират в ново обединение от типове.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
В този пример ToArray
се прилага към всеки член на обединението string | number
поотделно, което води до string[] | number[]
. Ако условието не беше дистрибутивно, резултатът щеше да бъде (string | number)[]
.
Използване на помощни типове (Utility Types)
TypeScript предоставя няколко вградени помощни типове (utility types), които използват Mapped Types и Conditional Types. Тези помощни типове могат да се използват като градивни елементи за по-сложни трансформации на типове.
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
е незадължително.
Използване на Template Literal Types
Template Literal Types ви позволяват да създавате типове, базирани на низови литерали. Те могат да се използват в комбинация с Mapped Types и Conditional Types за създаване на динамични и експресивни трансформации на типове. Например, можете да създадете тип, който добавя префикс към имената на всички свойства:
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
.
Най-добри практики и съображения
- Бъдете семпли: Въпреки че Mapped Types и Conditional Types са мощни, те могат също да направят кода ви по-сложен. Опитайте се да поддържате трансформациите на типовете възможно най-прости.
- Използвайте помощни типове: Възползвайте се от вградените помощни типове на TypeScript, когато е възможно. Те са добре тествани и могат да опростят кода ви.
- Документирайте типовете си: Документирайте ясно трансформациите на типовете си, особено ако са сложни. Това ще помогне на други разработчици да разберат кода ви.
- Тествайте типовете си: Използвайте проверката на типове в TypeScript, за да се уверите, че вашите трансформации на типове работят както се очаква. Можете да пишете единични тестове, за да проверите поведението на вашите типове.
- Вземете предвид производителността: Сложните трансформации на типове могат да повлияят на производителността на вашия TypeScript компилатор. Внимавайте със сложността на вашите типове и избягвайте ненужни изчисления.
Заключение
Mapped Types и Conditional Types са мощни функции в TypeScript, които ви позволяват да създавате изключително гъвкави и експресивни трансформации на типове. Овладявайки тези концепции, можете да подобрите типовата безопасност, поддръжката и цялостното качество на вашите TypeScript приложения. От прости трансформации като правене на свойства незадължителни или само за четене до сложни рекурсивни трансформации и условна логика, тези функции предоставят инструментите, от които се нуждаете, за да изграждате стабилни и мащабируеми приложения. Продължавайте да изследвате и експериментирате с тези функции, за да отключите пълния им потенциал и да станете по-опитен TypeScript разработчик.
Докато продължавате пътуването си с TypeScript, не забравяйте да се възползвате от изобилието от налични ресурси, включително официалната документация на TypeScript, онлайн общностите и проектите с отворен код. Прегърнете силата на Mapped Types и Conditional Types и ще бъдете добре подготвени да се справите дори с най-предизвикателните проблеми, свързани с типовете.