Загрузка…
Загрузка…
TypeScript · senior · сложность 8
T extends U ? X : Y — это if на уровне типов. Неочевидное поведение — distribution: если проверяемый тип — naked type parameter и аргумент — union, условие выполняется для каждого члена, результаты снова объединяются. infer — pattern-matching внутри extends: захватывает часть совпавшей формы в новую type variable.
Conditional type остаётся отложенным, пока не хватает информации. Внутри дженерика T extends string ? A : B ветка ещё не выбирается — ждёт конкретный T. Поэтому композируются ReturnType, Parameters, Awaited, NonNullable.
infer говорит: «совпади с формой, а то, что попало в слот infer X, привяжи к X» — деструктуризация для типов.
Distribution всех удивляет. Bare parameter (T extends …, не [T] extends …) + union → map по членам:
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>; // string[] | number[] ← NOT (string | number)[]Следствия: never — пустой union, distribution даёт never; boolean распадается на true | false и удваивает результат.
Выключить distribution — обернуть обе стороны в одноэлементный tuple:
type IsString<T> = [T] extends [string] ? true : false; // no distributionГде стоит infer, влияет на объединение кандидатов: в ковариантной позиции (элемент массива, return) — union; в контравариантной (параметры функции) — intersection:
type Elem<T> = T extends (infer E)[] ? E : never;
type U = Elem<(string | number)[]>; // string | number (union)
type Param<T> = T extends (arg: infer P) => any ? P : never;
type I = Param<((a: string) => void) & ((a: number) => void)>; // string & numberinfer … extends (TS 4.7) ограничивает захваченный тип:
type FirstChar<S> = S extends `${infer C extends string}${string}` ? C : never;В runtime conditionals стираются — никакого typeof. Решения в runtime пишите обычными guard'ами.
// Rebuild the standard-library conditionals — a frequent whiteboard task
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type MyNonNullable<T> = T extends null | undefined ? never : T;
type MyAwaited<T> = T extends Promise<infer V> ? MyAwaited<V> : T; // recurse to unwrap nesting
type R = MyReturnType<() => string>; // string
type A = MyAwaited<Promise<Promise<number>>>; // number// The distribution gotcha, made explicit
type Wrap<T> = T extends any ? { v: T } : never;
type Distributed = Wrap<'a' | 'b'>; // { v: 'a' } | { v: 'b' }
type Blocked = [('a' | 'b')] extends [any] ? { v: 'a' | 'b' } : never; // { v: 'a' | 'b' }
// Why NonNullable is written with a naked T: it must distribute to filter each member
type Clean = NonNullable<string | null | undefined>; // stringConditional types — ветвление на compile time с distribution по naked T; infer вытаскивает куски форм. Для «реального» выбора в runtime нужен JS-guard, не тип.