Conditional Types
Сердце advanced TS. На интервью обязательно: «Почему Exclude<‘a’|‘b’,‘a’> = ‘b’, а не never?» — потому что conditional дистрибутируется по голому type parameter в union. Красный флаг — не знать про обёртку [T] extends [U], отключающую distribution.
Базовый conditional:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hi'>; // truetype B = IsString<42>; // falsetype C = IsString<string>; // 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<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'type Y = Extract<'a' | 1 | 'b', string>; // 'a' | 'b'
// Отключаем distribution, оборачивая в tuple:type IsExactlyUnion<T> = [T] extends ['a' | 'b'] ? true : false;type T1 = IsExactlyUnion<'a'>; // false (не покрывает 'b')type T2 = IsExactlyUnion<'a' | 'b'>; // trueDistribution позволяет писать compose-типы вроде NonNullable:
type NonNullableX<T> = T extends null | undefined ? never : T;type Z = NonNullableX<string | number | null | undefined>; // string | numberИтог: Conditional + distribution даёт map/filter по union «бесплатно». Чтобы выключить распределение — оберни в кортеж.