Полное руководство по мощным сопоставленным и условным типам в TypeScript, включающее практические примеры и продвинутые сценарии использования для создания надежных и типобезопасных приложений.
Освоение сопоставленных и условных типов в TypeScript
TypeScript, надмножество JavaScript, предлагает мощные возможности для создания надежных и поддерживаемых приложений. Среди этих возможностей, сопоставленные типы (Mapped Types) и условные типы (Conditional Types) выделяются как незаменимые инструменты для продвинутых манипуляций с типами. Это руководство представляет собой исчерпывающий обзор этих концепций, исследуя их синтаксис, практическое применение и продвинутые сценарии использования. Независимо от того, являетесь ли вы опытным разработчиком на TypeScript или только начинаете свой путь, эта статья даст вам знания для эффективного использования этих возможностей.
Что такое сопоставленные типы?
Сопоставленные типы позволяют создавать новые типы путем преобразования существующих. Они перебирают свойства существующего типа и применяют преобразование к каждому свойству. Это особенно полезно для создания вариаций существующих типов, например, делая все свойства необязательными или доступными только для чтения.
Основной синтаксис
Синтаксис сопоставленного типа выглядит следующим образом:
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
.
Что такое условные типы?
Условные типы позволяют определять типы, которые зависят от некоторого условия. Они предоставляют способ выражать отношения между типами на основе того, удовлетворяет ли тип определенному ограничению. Это похоже на тернарный оператор в 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
Извлечение типа из объединения
Вы можете использовать условные типы для извлечения определенного типа из типа-объединения. Например, для извлечения типов, не являющихся 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 предоставляет несколько встроенных служебных типов, которые используют сопоставленные и условные типы. Эти служебные типы можно использовать как строительные блоки для более сложных преобразований типов.
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, онлайн-сообщества и проекты с открытым исходным кодом. Используйте мощь сопоставленных и условных типов, и вы будете хорошо подготовлены к решению даже самых сложных проблем, связанных с типами.