11. Условные типы (Conditional Types)
TypeScript: Броня. Урок 10: Условные типы (Conditional Types)
Заголовок раздела «TypeScript: Броня. Урок 10: Условные типы (Conditional Types)»Условные типы в TypeScript - это мощный инструмент, который позволяет создавать типы, зависящие от условий. Они работают по принципу тернарного оператора: T extends U ? X : Y. Если тип T совместим с типом U, то результат будет X, иначе - Y. Это открывает невероятные возможности для создания гибких и переиспользуемых типов.
Базовый синтаксис
Заголовок раздела «Базовый синтаксис»Синтаксис условных типов выглядит следующим образом:
T extends U ? X : YГде:
T- проверяемый типU- тип для сравненияX- результат, если условие истинноY- результат, если условие ложно
Простые примеры
Заголовок раздела «Простые примеры»// Пример 1: Проверка, является ли тип строкойtype IsString<T> = T extends string ? true : false;
type A = IsString<string>; // truetype B = IsString<number>; // falsetype C = IsString<"hello">; // true (строковый литерал)
// Пример 2: Извлечение типа из массиваtype ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArray = ArrayElementType<number[]>; // numbertype StringArray = ArrayElementType<string[]>; // stringtype NotArray = ArrayElementType<boolean>; // never
// Пример 3: Удаление null из типаtype NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null;type DefinitelyString = NonNullable<MaybeString>; // stringРаспределительные условные типы (Distributive Conditional Types)
Заголовок раздела «Распределительные условные типы (Distributive Conditional Types)»Когда условный тип применяется к union типу, TypeScript автоматически “распределяет” проверку на каждый член union:
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;// Результат: string[] | number[]// (а не (string | number)[])
// Пример с фильтрацией типовtype Filter<T, U> = T extends U ? T : never;
type OnlyStrings = Filter<string | number | boolean, string>;// Результат: string
type OnlyNumbers = Filter<string | number | boolean, number>;// Результат: numberПредотвращение распределения
Заголовок раздела «Предотвращение распределения»Иногда нужно избежать распределения. Для этого оборачиваем проверяемый тип в кортеж:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result1 = ToArrayNonDist<string | number>;// Результат: (string | number)[]
type ToArray<T> = T extends any ? T[] : never;type Result2 = ToArray<string | number>;// Результат: string[] | number[]Практические примеры
Заголовок раздела «Практические примеры»// Извлечение типов функцийtype FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never;}[keyof T];
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K;}[keyof T];
interface User { name: string; age: number; greet(): void; login(): Promise<void>;}
type UserFunctions = FunctionPropertyNames<User>;// Результат: "greet" | "login"
type UserData = NonFunctionPropertyNames<User>;// Результат: "name" | "age"
// Unwrap Promisetype UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // stringtype B = UnwrapPromise<number>; // number
// Flatten nested arraystype Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Nested = Flatten<string[][][]>; // stringЖизненный пример
Заголовок раздела «Жизненный пример»В реальных проектах условные типы часто используются для создания type-safe API:
// Система типизированных событийtype EventMap = { 'user:login': { userId: string; timestamp: number }; 'user:logout': { userId: string }; 'data:update': { key: string; value: any };};
type EventName = keyof EventMap;
// Условный тип для получения типа payloadtype EventPayload<T extends EventName> = EventMap[T];
class EventEmitter { on<T extends EventName>( event: T, handler: (payload: EventPayload<T>) => void ): void { // implementation }
emit<T extends EventName>(event: T, payload: EventPayload<T>): void { // implementation }}
const emitter = new EventEmitter();
// TypeScript знает точный тип payload для каждого событияemitter.on('user:login', (data) => { console.log(data.userId, data.timestamp); // ✓ типы известны});
emitter.emit('user:login', { userId: '123', timestamp: Date.now()}); // ✓ требует правильный payload
// emitter.emit('user:login', { userId: '123' }); // ✗ ошибка - нет timestampКомбинирование с другими продвинутыми типами
Заголовок раздела «Комбинирование с другими продвинутыми типами»// Создание ReadonlyDeep типаtype ReadonlyDeep<T> = T extends object ? { readonly [K in keyof T]: ReadonlyDeep<T[K]> } : T;
interface Config { database: { host: string; port: number; credentials: { user: string; password: string; }; };}
type ImmutableConfig = ReadonlyDeep<Config>;// Все вложенные свойства станут readonly
// Извлечение async функцийtype AsyncFunctionKeys<T> = { [K in keyof T]: T[K] extends (...args: any[]) => Promise<any> ? K : never;}[keyof T];
interface API { fetchUser(): Promise<User>; logout(): void; updateProfile(): Promise<void>; getName(): string;}
type AsyncMethods = AsyncFunctionKeys<API>;// Результат: "fetchUser" | "updateProfile"Ключевые моменты
Заголовок раздела «Ключевые моменты»- Условные типы позволяют создавать типы на основе условий, используя синтаксис
T extends U ? X : Y - При применении к union типам происходит автоматическое распределение (distributive behavior)
- Ключевое слово
inferпозволяет извлекать типы внутри условных типов (подробнее в следующих уроках) - Условные типы - основа многих встроенных utility типов TypeScript
- Можно предотвратить распределение, оборачивая типы в кортежи
- Используются для создания мощных generic типов и type-safe API
- Комбинируются с mapped types для создания сложных трансформаций типов
- Критически важны для библиотек и фреймворков, работающих с динамическими типами