Освой дистрибутивность условных типов, сопоставление с образцом через infer и крайние случаи вроде never, any и boolean.
Открыть этот урок в KodokonУсловные типы - это механизм ветвления в системе типов: T extends U ? X : Y спрашивает у компилятора, присваиваем ли T к U, а затем выбирает ветку. Все стандартные утилиты вроде ReturnType, Exclude или Awaited построены на них. На этом уровне вопрос уже не в том, как их писать, а в том, как точно предсказать, как компилятор их вычисляет - особенно когда в игре объединения и never.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">;
// true
type B = IsString<string | number>;
// boolean, not false!Ключевое поведение - это дистрибутивность: когда проверяемый тип является голым параметром типа (не обёрнут в кортеж, массив или пересечение), условие применяется отдельно к каждому члену объединения, а результаты затем соединяются. Поэтому IsString<string | number> вычисляет true | false, то есть boolean. Чтобы отключить это распределение, оберни обе стороны в кортеж: [T] extends [U]. Один грозный крайний случай: never - это пустое объединение; распределение по нулю членов не вычисляет ни одной ветки, и результат равен never, какую бы ветку ты ни написал.
type ToArray<T> = T extends unknown ? T[] : never;
type NoDist<T> = [T] extends [unknown] ? T[] : never;
type A = ToArray<string | number>;
// string[] | number[]
type B = NoDist<string | number>;
// (string | number)[]
type IsNever<T> = [T] extends [never] ? true : false;
type C = IsNever<never>; // true thanks to the tupleКлючевое слово infer вводит переменную типа, захваченную через сопоставление с образцом: компилятор пытается сопоставить структуру проверяемого типа с образцом и связывает переменную, если совпадение удалось. Можно поставить несколько объявлений infer с одним и тем же именем: в ковариантной позиции (производимые значения, например возвращаемый тип) кандидаты сливаются в объединение; в контравариантной позиции (параметры функции) они соединяются в пересечение. Это правило напрямую следует из вариантности: в каждом направлении это единственный безопасный выбор.
type ElementOf<T> =
T extends readonly (infer E)[] ? E : never;
type FirstParam<T> =
T extends (arg: infer P) => unknown ? P : never;
type Merged<T> =
T extends { a: (x: infer P) => void;
b: (x: infer P) => void }
? P : never;
type M = Merged<{
a: (x: { id: string }) => void;
b: (x: { tag: string }) => void;
}>; // { id: string } & { tag: string }Начиная с TypeScript 4.7 infer принимает встроенное ограничение: infer N extends number фильтрует захват, а с версии 4.8 компилятор даже преобразует числовой строковый литерал в числовой литерал. Ещё одна тонкость спецификации: условный тип, во входных данных которого остаётся неразрешённый обобщённый параметр, остаётся отложенным; он вычисляется только в месте инстанцирования - это объясняет, почему некоторые ошибки всплывают только на стороне вызывающего кода.
type ToNumber<S extends string> =
S extends `${infer N extends number}` ? N : never;
type Port = ToNumber<"8080">; // the literal 8080
type Bad = ToNumber<"abc">; // neverExclude<boolean, true>?type IsNever<T> = T extends never ? true : false;, чему равен IsNever<never>?infer P с одинаковым именем, поставленные в позицию параметра функции, дают: