Conditional Types в TypeScript
Что такое Conditional Types?
Conditional Types (Условные типы) — это конструкция в TypeScript, которая позволяет выбирать тип на основе условия. Работают по принципу тернарного оператора, но для типов.
Синтаксис
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
type C = IsString<'hello'>; // true
Как это работает?
- Проверяется, является ли
Tподтипомstring - Если да — возвращается
true - Если нет — возвращается
false
Практические примеры
Извлечение типа возвращаемого значения
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUserName() {
return 'John';
}
function getUserAge() {
return 25;
}
type NameType = ReturnType<typeof getUserName>; // string
type AgeType = ReturnType<typeof getUserAge>; // number
Фильтрация типов
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined>; // number
type C = NonNullable<boolean | null | undefined>; // boolean
Проверка на массив
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<number[]>; // true
type B = IsArray<string>; // false
type C = IsArray<[1, 2, 3]>; // true (tuple тоже массив)
Distributive Conditional Types
Когда условный тип применяется к union type, он распределяется по каждому члену объединения.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// string extends any ? string[] : never | number extends any ? number[] : never
// string[] | number[]
Как это работает?
ToArray<string | number>
// Распределяется как:
= ToArray<string> | ToArray<number>
= string[] | number[]
Отключение распределения
Используйте квадратные скобки для предотвращения распределения:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type A = ToArrayNonDist<string | number>; // (string | number)[]
Ключевое слово infer
infer позволяет "извлечь" тип из структуры во время проверки условия.
Извлечение типа из Promise
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<Promise<number>>; // number
type C = Unwrap<boolean>; // boolean
Извлечение типов аргументов функции
type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
function test(name: string, age: number) {
return { name, age };
}
type First = FirstArg<typeof test>; // string
Извлечение типа элемента массива
type ElementType<T> = T extends (infer E)[] ? E : T;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // boolean
Вложенные условные типы
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type A = TypeName<string>; // "string"
type B = TypeName<42>; // "number"
type C = TypeName<() => void>; // "function"
type D = TypeName<{}>; // "object"
Встроенные утилиты на основе Conditional Types
TypeScript предоставляет множество встроенных утилит, реализованных через условные типы.
Exclude
Исключает типы из union:
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type B = Exclude<string | number, string>; // number
Extract
Извлекает типы из union:
type Extract<T, U> = T extends U ? T : never;
type A = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
type B = Extract<string | number, number>; // number
NonNullable
Удаляет null и undefined:
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined | null>; // number
ReturnType
Извлекает тип возвращаемого значения функции:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
function getUserData() {
return { name: 'John', age: 25 };
}
type UserData = ReturnType<typeof getUserData>;
// { name: string; age: number; }
Продвинутые паттерны
Рекурсивные условные типы
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface User {
name: string;
address: {
city: string;
country: string;
};
}
type ReadonlyUser = DeepReadonly<User>;
/*
{
readonly name: string;
readonly address: {
readonly city: string;
readonly country: string;
};
}
*/
Извлечение ключей определенного типа
type KeysOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
interface User {
id: number;
name: string;
age: number;
email: string;
}
type StringKeys = KeysOfType<User, string>; // "name" | "email"
type NumberKeys = KeysOfType<User, number>; // "id" | "age"
Создание Required полей по условию
type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
interface User {
name?: string;
age?: number;
email?: string;
}
type UserWithName = RequireKeys<User, 'name'>;
// { name: string; age?: number; email?: string; }
Практические кейсы
API Response Helper
type ApiResponse<T> = T extends { data: infer D }
? D
: T;
interface SuccessResponse {
data: { id: number; name: string };
status: 'success';
}
type Data = ApiResponse<SuccessResponse>;
// { id: number; name: string; }
Flatten Union Types
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number[][]>; // number[]
type C = Flatten<(string | number)[]>; // string | number
3. Promise Chain Type
type UnwrapPromise<T> = T extends Promise<infer U>
? UnwrapPromise<U>
: T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<Promise<Promise<Promise<boolean>>>>; // boolean
Function or Value
type ValueOrFunction<T> = T | (() => T);
type Resolve<T> = T extends (...args: any[]) => infer R ? R : T;
type A = Resolve<string>; // string
type B = Resolve<() => number>; // number
Ограничения и особенности
Глубина рекурсии
TypeScript ограничивает глубину рекурсивных типов для предотвращения бесконечных циклов:
// Может привести к ошибке "Type instantiation is excessively deep"
type DeepArray<T, N extends number = 10> =
N extends 0 ? T : DeepArray<T[], Decrement<N>>;
Порядок проверки имеет значение
// Порядок важен
type TypeName<T> =
T extends any[] ? "array" : // Сначала проверяем массив
T extends object ? "object" : // Потом объект
T extends string ? "string" :
"other";
type A = TypeName<string[]>; // "array" (не "object")
Never в условных типах
type A = never extends string ? true : false; // true
// never является подтипом любого типа
Сравнение с другими подходами
До Conditional Types
// Приходилось использовать перегрузки
function wrap(x: string): string[];
function wrap(x: number): number[];
function wrap(x: any): any[] {
return [x];
}
С Conditional Types
type Wrap<T> = T extends any ? T[] : never;
function wrap<T>(x: T): Wrap<T> {
return [x] as Wrap<T>;
}
const a = wrap('hello'); // string[]
const b = wrap(42); // number[]
Типичные ошибки
Забыть про distributivity
type WrapInArray<T> = T extends any ? T[] : never;
type A = WrapInArray<string | number>; // string[] | number[]
// Ожидали (string | number)[], но получили union массивов
// Правильно:
type WrapInArray<T> = [T] extends [any] ? T[] : never;
type B = WrapInArray<string | number>; // (string | number)[]
Неправильное использование infer
// Неправильно
type Wrong<T> = T extends infer U ? U : never; // Бессмысленно
// Правильно
type Correct<T> = T extends Promise<infer U> ? U : T;
Сложная логика в одном типе
// Плохо - сложно читать
type Complex<T> = T extends string ? T extends `${infer F}${infer R}` ? F extends 'a' ? true : false : false : false;
// Хорошо - разбить на части
type StartsWithA<T> = T extends `a${string}` ? true : false;
type IsString<T> = T extends string ? true : false;
type Complex<T> = IsString<T> extends true ? StartsWithA<T> : false;
Вывод
Conditional Types:
- Позволяют создавать гибкие и переиспользуемые типы
- Основа для многих встроенных утилит TypeScript
- Распределяются по union типам (distributive)
- Поддерживают извлечение типов через
infer - Могут быть рекурсивными (с ограничениями)
- Критически важны для продвинутой типизации
На собеседовании:
Важно уметь:
- Объяснить синтаксис
T extends U ? X : Y - Показать разницу между distributive и non-distributive типами
- Использовать
inferдля извлечения типов - Привести примеры встроенных утилит
- Реализовать собственные утилиты на основе условных типов