Conditional Types
Conditional types — типы с условной логикой:
T extends U ? X : Y. Позволяют выбирать тип в зависимости от условия.
Зачем нужно
- Создание типов с условной логикой: «если T — строка, то X, иначе Y»
- Извлечение вложенных типов через
infer - Фильтрация union типов
- Основа для многих встроенных utility types (ReturnType, Extract, Exclude)
Где используется
- Утилитарные типы:
Extract,Exclude,ReturnType,Parameters - Условная типизация возвращаемых значений
- Извлечение типов из сложных структур
- Типобезопасные API с перегрузками
Предпосылки
- Generics — параметрические типы
- Union и Intersection — union для distributive conditionals
Базовый синтаксис
// T extends U ? X : Y
// Если T совместим с U → тип X, иначе → тип Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
type C = IsString<string>; // true
// Практический пример
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: T extends Function
? "function"
: "object";
type A = TypeName<"hello">; // "string"
type B = TypeName<42>; // "number"
type C = TypeName< => void>; // "function"
type D = TypeName<{ x: 1 }>; // "object"
Ключевое слово infer
infer объявляет переменную типа внутри условия, позволяя «извлечь» часть типа:
// Извлечь тип элемента массива
type ElementOf<T> = T extends (infer U) ? U : T;
type A = ElementOf<string>; // string
type B = ElementOf<number>; // number
type C = ElementOf<string>; // string (не массив — возвращаем T)
// Извлечь тип из Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<number>>; // number
type C = UnwrapPromise<string>; // string
// Рекурсивная распаковка (как Awaited)
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type A = DeepUnwrap<Promise<Promise<Promise<string>>>>; // string
// Извлечь return type
type MyReturnType<T> = T extends (...args: any) => infer R ? R : never;
type A = MyReturnType< => string>; // string
type B = MyReturnType<(x: number) => void>; // void
// Извлечь параметры
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type A = MyParameters<(a: string, b: number) => void>; // [string, number]
// Извлечь первый параметр
type FirstParam<T> = T extends (first: infer F, ...rest: any) => any ? F : never;
type A = FirstParam<(name: string, age: number) => void>; // string
infer с constraints
// infer U extends string — U должен быть подтипом string
type GetString<T> = T extends { value: infer U extends string } ? U : never;
type A = GetString<{ value: "hello" }>; // "hello"
type B = GetString<{ value: 42 }>; // never (42 не extends string)
Distributive Conditional Types
Когда conditional type применяется к union, он распределяется по каждому члену union:
type ToArray<T> = T extends any ? T : never;
// С union:
type A = ToArray<string | number>;
// (string extends any ? string : never) | (number extends any ? number : never)
// = string | number
// БЕЗ distributive (обернуть в кортеж):
type ToArrayNonDist<T> = [T] extends [any] ? T : never;
type B = ToArrayNonDist<string | number>;
// [string | number] extends [any] ? (string | number) : never
// = (string | number)
Фильтрация union через distribute
// Extract — оставить совместимые
type Extract<T, U> = T extends U ? T : never;
type A = Extract<"a" | "b" | "c" | 1 | 2, string>;
// "a" extends string → "a"
// "b" extends string → "b"
// "c" extends string → "c"
// 1 extends string → never
// 2 extends string → never
// Результат: "a" | "b" | "c"
// Exclude — убрать совместимые
type Exclude<T, U> = T extends U ? never : T;
type B = Exclude<"a" | "b" | 1 | 2, string>;
// Результат: 1 | 2
// NonNullable
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null | undefined>;
// string
Практические примеры
Условный return type
// Перегрузка через conditional type
function process<T extends string | number>(
value: T
): T extends string ? string : number {
if (typeof value === "string") {
return value.split("") as any;
}
return (value * 2) as any;
}
const a = process("hello"); // string
const b = process(42); // number
Фильтрация свойств объекта по типу значения
// Ключи с определённым типом значения
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
interface User {
id: number;
name: string;
email: string;
age: number;
isAdmin: boolean;
}
type StringKeys = KeysOfType<User, string>;
// "name" | "email"
type NumberKeys = KeysOfType<User, number>;
// "id" | "age"
// Pick только строковые поля
type StringFields = Pick<User, KeysOfType<User, string>>;
// { name: string; email: string }
Рекурсивный Flatten
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type A = Flatten<number>; // number
type B = Flatten<string>; // string
type C = Flatten<number>; // number
// Flatten объекта (на один уровень)
type FlattenObject<T> = {
[K in keyof T]: T[K] extends object
? T[K] extends any
? T[K]
: keyof T[K] extends never
? T[K]
: T[K]
: T[K];
};
Вычисление типа из конфигурации
interface FieldConfig {
type: "string" | "number" | "boolean";
required: boolean;
}
type FieldType<F extends FieldConfig> = (F["type"] extends "string"
? string
: F["type"] extends "number"
? number
: boolean) extends infer T
? F["required"] extends true
? T
: T | undefined
: never;
type A = FieldType<{ type: "string"; required: true }>; // string
type B = FieldType<{ type: "number"; required: false }>; // number | undefined
type C = FieldType<{ type: "boolean"; required: true }>; // boolean
Частые ошибки
- Забыть про distributive behavior — union распределяется автоматически
type IsNever<T> = T extends never ? true : false;
type A = IsNever<never>; // never (не true!)
// never — пустой union, distribute ничего не даёт
// Правильно:
type IsNever<T> = [T] extends [never] ? true : false;
type B = IsNever<never>; // true
- Слишком глубокая рекурсия — TypeScript ограничивает глубину
inferв неправильной позиции — infer работает только внутриextendsв conditional type- Путать extends в conditional и constraint —
T extends U ? X : Y(условие) vsT extends U(ограничение) - Не использовать
as anyв реализации — TypeScript не может сузить тип внутри функции по conditional return
Практика
- Реализуйте
MyReturnType<T>иMyParameters<T>через infer - Создайте
Flatten<T>— рекурсивно распаковывает массивы - Напишите
IsArray<T>— возвращаетtrueесли T массив, иначеfalse - Реализуйте
KeysOfType<T, V>— ключи объекта с определённым типом значения - Создайте
NonDistributive<T>— обёртка для отключения distributive behavior
Связанные темы
- Generics — основа conditional types
- Mapped types — комбинирование с mapped types
- Utility types — реализованы через conditional types
- Template literal types — строковые трансформации