Что такое conditional types в TypeScript?
T extends U ? X : Y. Они работают аналогично тернарному оператору, но на уровне системы типов.Conditional Types в TypeScript
Conditional types позволяют задавать типы, которые зависят от условия. Синтаксис аналогичен тернарному оператору:
T extends U ? X : Y
Если тип T совместим с типом U, результирующий тип — X, иначе — Y.
Базовое использование
Простой пример — определение типа на основе входного параметра:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Distributive Conditional Types
Когда условный тип применяется к union-типу, он автоматически распределяется по каждому члену union. Это называется дистрибутивностью:
type ToArray<T> = T extends unknown ? T[] : never;
// Распределяется: string[] | number[], а не (string | number)[]
type Result = ToArray<string | number>;
Чтобы отключить дистрибутивность, нужно обернуть тип в кортеж:
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
// Теперь: (string | number)[]
type Result = ToArrayNonDist<string | number>;
Ключевое слово infer
infer позволяет извлекать типы прямо внутри условного типа, объявляя переменную типа в позиции extends:
// Извлекаем тип возвращаемого значения функции
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (x: number) => string;
type FnReturn = ReturnType<Fn>; // string
// Извлекаем тип элемента массива
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type Unpacked = UnpackArray<number[]>; // number
Встроенные утилиты на основе conditional types
Многие стандартные утилиты TypeScript реализованы через conditional types:
// Exclude — исключает из T типы, совместимые с U
type Exclude<T, U> = T extends U ? never : T;
type T1 = Exclude<string | number | boolean, string>; // number | boolean
// Extract — оставляет только типы из T, совместимые с U
type Extract<T, U> = T extends U ? T : never;
type T2 = Extract<string | number | boolean, string>; // string
// NonNullable — убирает null и undefined
type NonNullable<T> = T extends null | undefined ? never : T;
Рекурсивные conditional types
Начиная с TypeScript 4.1, conditional types поддерживают рекурсию:
// Рекурсивное извлечение типа из вложенных массивов
type DeepUnpack<T> = T extends (infer U)[] ? DeepUnpack<U> : T;
type Deep = DeepUnpack<number[][][]>; // number
Практический паттерн: фильтрация ключей объекта
// Оставляем только ключи, значения которых — строки
type StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
interface User {
name: string;
age: number;
email: string;
}
type UserStringKeys = StringKeys<User>; // 'name' | 'email'
Что хочет услышать интервьюер
Знание синтаксиса `T extends U ? X : Y` и понимание аналогии с тернарным оператором
Объяснение дистрибутивности conditional types при работе с union-типами и способ её отключения через кортеж
Уверенное владение ключевым словом `infer` для извлечения вложенных типов
Понимание, как реализованы стандартные утилиты (`ReturnType`, `Exclude`, `Extract`, `NonNullable`) через conditional types
Знание ограничений: рекурсия, отложенное вычисление (deferred), поведение в дженериках
Пример: Базовый синтаксис
// Базовый синтаксис
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<string>; // false
Пример: Ключевое слово infer
// infer — извлечение типа изнутри
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type Resolved = UnpackPromise<Promise<number>>; // number
type Plain = UnpackPromise<string>; // string
Пример: Дистрибутивность и её отключение
// Дистрибутивность: условный тип распределяется по union
type Wrap<T> = T extends unknown ? { value: T } : never;
// { value: string } | { value: number } — не { value: string | number }
type Wrapped = Wrap<string | number>;
// Отключение дистрибутивности через кортеж
type WrapNonDist<T> = [T] extends [unknown] ? { value: T } : never;
// { value: string | number }
type WrappedSingle = WrapNonDist<string | number>;
Пример: Фильтрация ключей объекта
// Фильтрация ключей объекта по типу значения
type KeysOfType<Obj, ValueType> = {
[K in keyof Obj]: Obj[K] extends ValueType ? K : never;
}[keyof Obj];
interface Config {
host: string;
port: number;
debug: boolean;
name: string;
}
// 'host' | 'name'
type StringProps = KeysOfType<Config, string>;
Типичные ошибки
Не понимают дистрибутивность — удивляются, что `T extends unknown ? T[] : never` для union даёт массив каждого члена, а не массив union
Путают `infer` с обычной переменной типа — пытаются использовать `infer` вне extends-клаузулы
Забывают, что conditional type в дженерике вычисляется отложенно (deferred) и не разрешается до передачи конкретного типа
Неправильно используют `never` в условных типах — не понимают, что `never` в union просто исчезает
Не знают, как отключить дистрибутивность через обёртку в кортеж `[T] extends [U]`


