Это «программирование на типах»: условия, циклы по ключам и сопоставление с образцом. На этом построены все утилитарные типы.
keyof, typeof, индексный доступ
type User = { id: number; name: string };
type Keys = keyof User; // 'id' | 'name'
type Name = User['name']; // string
type Values = User[keyof User]; // number | string
const config = { port: 3000 };
type Config = typeof config; // { port: number }
const roles = ['admin', 'user'] as const;
type Role = (typeof roles)[number]; // 'admin' | 'user'
Mapped-типы
Проходят по объединению ключей и строят новый объект:
type Flags<T> = { [K in keyof T]: boolean };
Модификаторы: readonly и ? можно добавить (+, по умолчанию) или снять (-):
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Concrete<T> = { [K in keyof T]-?: T[K] };
Mapped-тип вида { [K in keyof T]: ... } гомоморфный: он сохраняет ? и readonly исходника, а для массивов и кортежей возвращает массив или кортеж.
Переназначение ключей через as
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type G = Getters<User>; // { getId: () => number; getName: () => string }
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
Ключ, переназначенный в never, выбрасывается: так фильтруют поля.
Условные типы
type IsString<T> = T extends string ? true : false;
Если T это «голый» параметр, а на вход пришло объединение, условие применяется к каждому члену и результаты объединяются (дистрибутивность):
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>; // string[] | number[]
Чтобы отключить распределение, оберните обе стороны в кортеж:
type ToArrayOne<T> = [T] extends [any] ? T[] : never;
type B = ToArrayOne<string | number>; // (string | number)[]
Особый случай: never это пустое объединение, поэтому IsString<never> даёт never, а не false.
infer
infer объявляет переменную типа внутри условия и вытаскивает часть:
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type Ret<T> = T extends (...args: any[]) => infer R ? R : never;
type First<T> = T extends [infer F, ...unknown[]] ? F : never;
Шаблонные литеральные типы
type Event = 'click' | 'focus';
type Handler = `on${Capitalize<Event>}`; // 'onClick' | 'onFocus'
type Route = `/users/${number}`;
Объединения в шаблоне перемножаются: `${'a'|'b'}-${'x'|'y'}` даёт четыре строки.
Рекурсия
Типы могут ссылаться на себя:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
У рекурсии есть предел глубины, а слишком большие объединения дают ошибку «Expression produces a union type that is too complex».
Частые ошибки
- Ожидать
falseотIsString<string | number>: из-за распределения получитсяboolean, то естьtrue | false. - Забыть
string & Kв шаблоне:keyof Tсодержит иnumber | symbol. - Писать
T extends objectв DeepReadonly и удивляться, что функции тоже «заморожены» как объекты.
Что спросят на собесе
- Что такое дистрибутивный условный тип и как его отключить.
- Для чего
infer, напишитеReturnType. - Как mapped-тип снимает
readonlyи?. - Как отфильтровать поля объекта по типу значения.
- Как получить объединение значений массива
as const.