Умовний тип вибирає один з двох типів залежно від перевірки - тернарний оператор на рівні типів:
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
Практичні приклади:
1. Тип результату залежно від аргументу:
type ApiResult<T extends 'one' | 'many'> = T extends 'one' ? User : User[];
declare function fetchUsers<T extends 'one' | 'many'>(mode: T): Promise<ApiResult<T>>;
const one = await fetchUsers('one'); // User
const many = await fetchUsers('many'); // User[]
2. Фільтрація об'єднань - так влаштовані вбудовані Exclude і Extract:
type Exclude<T, U> = T extends U ? never : T;
type Events = 'click' | 'focus' | 'keydown' | 'keyup';
type KeyEvents = Extract<Events, `key${string}`>; // 'keydown' | 'keyup'
type Other = Exclude<Events, `key${string}`>; // 'click' | 'focus'
never у гілці «викидає» член об'єднання.
3. NonNullable<T>, ReturnType<T>, Awaited<T> - теж умовні типи (останні два - з infer).
Особливість - розподіл по об'єднаннях: якщо перевіряється «голий» параметр типу, а передано об'єднання, умова застосовується до кожного члена окремо:
type ToArray<T> = T extends unknown ? T[] : never;
type R = ToArray<string | number>; // string[] | number[], а не (string | number)[]
Обмеження й поради:
- читабельність: вкладені умовні типи на кілька рівнів важко читати й підтримувати. Розбивайте на іменовані проміжні типи;
- у реалізації функції TypeScript не може звузити умовний тип повернення за аргументом - доводиться використовувати перевантаження функції або явне приведення в реалізації;
- глибина рекурсії обмежена: для рекурсивних умовних типів компілятор зупиняється з помилкою «Type instantiation is excessively deep».
Коли умовні типи доречні: бібліотеки й утиліти, типи API-клієнтів, похідні типи. У прикладному коді частіше вистачає об'єднань, перевантажень і узагальнень без умов.