Перейти к содержимому

Conditional Types

Сердце advanced TS. На интервью обязательно: «Почему Exclude<‘a’|‘b’,‘a’> = ‘b’, а не never?» — потому что conditional дистрибутируется по голому type parameter в union. Красный флаг — не знать про обёртку [T] extends [U], отключающую distribution.

Базовый conditional:

type IsString&lt;T&gt; = T extends string ? true : false;
type A = IsString&lt;'hi'&gt;; // true
type B = IsString&lt;42&gt;; // false
type C = IsString&lt;string&gt;; // true

Дистрибутивность: при «голом» type parameter conditional раскладывается по union.

  Exclude<T, U> = T extends U ? never : T

T = ‘a’ | ‘b’ | ‘c’,  U = ‘a’
│ distribute over union
▼
(‘a’ extends ‘a’ ? never : ‘a’)   = never
| (‘b’ extends ‘a’ ? never : ‘b’)   = ‘b’
| (‘c’ extends ‘a’ ? never : ‘c’)   = ‘c’
│ collapse
▼
never | ‘b’ | ‘c’  =  ‘b’ | ‘c’
// Стандартные библиотечные:
type X = Exclude&lt;'a' | 'b' | 'c', 'a'&gt;; // 'b' | 'c'
type Y = Extract&lt;'a' | 1 | 'b', string&gt;; // 'a' | 'b'
// Отключаем distribution, оборачивая в tuple:
type IsExactlyUnion&lt;T&gt; = [T] extends ['a' | 'b'] ? true : false;
type T1 = IsExactlyUnion&lt;'a'&gt;; // false (не покрывает 'b')
type T2 = IsExactlyUnion&lt;'a' | 'b'&gt;; // true

Distribution позволяет писать compose-типы вроде NonNullable:

type NonNullableX&lt;T&gt; = T extends null | undefined ? never : T;
type Z = NonNullableX&lt;string | number | null | undefined&gt;; // string | number

Итог: Conditional + distribution даёт map/filter по union «бесплатно». Чтобы выключить распределение — оберни в кортеж.