Български

Изчерпателно ръководство за мощните 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;
};

Практически примери

Правене на свойствата само за четене

Да приемем, че имате интерфейс, представляващ потребителски профил:

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

Практически примери

Определяне дали даден тип е низ

Нека създадем тип, който връща 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. Тези помощни типове могат да се използват като градивни елементи за по-сложни трансформации на типове.

Тези помощни типове са мощни инструменти, които могат да опростят сложни манипулации на типове. Например, можете да комбинирате 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 разработчик.

Докато продължавате пътуването си с TypeScript, не забравяйте да се възползвате от изобилието от налични ресурси, включително официалната документация на TypeScript, онлайн общностите и проектите с отворен код. Прегърнете силата на Mapped Types и Conditional Types и ще бъдете добре подготвени да се справите дори с най-предизвикателните проблеми, свързани с типовете.