Русский

Полное руководство по мощным сопоставленным и условным типам в TypeScript, включающее практические примеры и продвинутые сценарии использования для создания надежных и типобезопасных приложений.

Освоение сопоставленных и условных типов в TypeScript

TypeScript, надмножество JavaScript, предлагает мощные возможности для создания надежных и поддерживаемых приложений. Среди этих возможностей, сопоставленные типы (Mapped Types) и условные типы (Conditional Types) выделяются как незаменимые инструменты для продвинутых манипуляций с типами. Это руководство представляет собой исчерпывающий обзор этих концепций, исследуя их синтаксис, практическое применение и продвинутые сценарии использования. Независимо от того, являетесь ли вы опытным разработчиком на TypeScript или только начинаете свой путь, эта статья даст вам знания для эффективного использования этих возможностей.

Что такое сопоставленные типы?

Сопоставленные типы позволяют создавать новые типы путем преобразования существующих. Они перебирают свойства существующего типа и применяют преобразование к каждому свойству. Это особенно полезно для создания вариаций существующих типов, например, делая все свойства необязательными или доступными только для чтения.

Основной синтаксис

Синтаксис сопоставленного типа выглядит следующим образом:

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.

Что такое условные типы?

Условные типы позволяют определять типы, которые зависят от некоторого условия. Они предоставляют способ выражать отношения между типами на основе того, удовлетворяет ли тип определенному ограничению. Это похоже на тернарный оператор в JavaScript, но для типов.

Основной синтаксис

Синтаксис условного типа выглядит следующим образом:

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

Извлечение типа из объединения

Вы можете использовать условные типы для извлечения определенного типа из типа-объединения. Например, для извлечения типов, не являющихся nullable:

type NonNullable<T> = T extends null | undefined ? never : T;

type Result4 = NonNullable<string | null | undefined>; // string

Здесь, если T — это null или undefined, тип становится never, который затем отфильтровывается при упрощении типов-объединений в TypeScript.

Вывод типов

Условные типы также можно использовать для вывода типов с помощью ключевого слова 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

Частым сценарием использования является создание типа, который делает все свойства объекта, включая вложенные, доступными только для чтения. Этого можно достичь с помощью рекурсивного условного типа.

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 предоставляет несколько встроенных служебных типов, которые используют сопоставленные и условные типы. Эти служебные типы можно использовать как строительные блоки для более сложных преобразований типов.

Эти служебные типы являются мощными инструментами, которые могут упростить сложные манипуляции с типами. Например, вы можете скомбинировать 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 | MLOG