Сопоставленный тип строит новый объектный тип, перебирая набор ключей. { [K in keyof T]: boolean } означает «для каждого ключа K из T свойство с именем K и типом boolean»:
Добавьте ключ в Features, и Flags<Features> тоже его потребует. Сопоставленные типы существуют только при компиляции: они описывают объекты, но не создают их.
Синтаксис
У сопоставленного типа три части: переменная ключа, объединение ключей для перебора и тип свойства, который может использовать переменную ключа.
type MappedType = {
[K in Keys]: PropertyType; // K takes each member of Keys in turn
};
Keys может быть любым объединением строк, чисел или символов. Он не обязан происходить из keyof:
Именно это делает Record<Size, number>; Record это сопоставленный тип в одну строку. Тип свойства может использовать ключ: { [K in keyof T]: T[K] } копирует тип каждого свойства через доступ по индексу, а { [K in keyof T]: T[K] | null } делает каждое свойство допускающим null.
Модификаторы: readonly и ? с + и -
Сопоставленный тип может добавить или убрать модификаторы readonly и необязательности (?) сразу у всех свойств. Префикс + добавляет (он подразумевается, если ничего не написать), а - убирает:
Последняя строка показывает, что readonly это правило только времени компиляции: о присваивании было сообщено (здесь подавлено через @ts-expect-error), но сгенерированный JavaScript всё равно его выполнил. -? делает больше, чем убирает вопросительный знак: он также убирает undefined из типа свойства, поэтому AllRequired<Account> отвергает { id: 1, email: undefined } с ошибкой error TS2322: Type 'undefined' is not assignable to type 'string'.
Как устроены Partial, Readonly, Pick и Record
Встроенные служебные типы, которые меняют форму объектов, это сопоставленные типы. Вот определения из собственного lib.es5.d.ts TypeScript:
type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, T> = { [P in K]: T };
Читать их полезно: Pick перебирает только переданные ключи, а Record игнорирует исходный тип и даёт каждому ключу один и тот же тип значения. Omit не является самостоятельным сопоставленным типом: это Pick<T, Exclude<keyof T, K>>. Все они перечислены в справочнике по служебным типам.
Переназначение ключей через as
Конструкция as после ключа меняет имя свойства. В сочетании с шаблонными литеральными типами она может порождать новые имена из старых:
string & K нужен потому, что keyof T может включать ключи number и symbol, а Capitalize принимает только строки. Пересечение оставляет строковые ключи и отбрасывает остальные.
Фильтрация ключей через never
Если конструкция as даёт never для ключа, этот ключ удаляется. С условным типом можно оставлять или отбрасывать свойства в зависимости от их типа:
Типы вроде DataOnly полезны, чтобы описать, что переживает JSON.stringify или что редактирует форма: поля без методов.
Какие модификаторы сохраняются
Сопоставленный тип по keyof T для некоторого типа T называется гомоморфным, и он копирует модификаторы readonly и ? каждого свойства из T. Сопоставленный тип по простому объединению ключей начинается без модификаторов:
type Account = { readonly id: number; email?: string };
type Copy<T> = { [K in keyof T]: T[K] };
type A = Copy<Account>;
// { readonly id: number; email?: string } (modifiers kept)
type B = { [K in "id" | "email"]: Account[K] };
// { id: number; email: string | undefined } (email is required now)
В B тип email по-прежнему содержит undefined, потому что его включает Account["email"], но само свойство больше не необязательное: { id: 1 } отвергается. Поэтому Partial<T> и подобные ему перебирают keyof T: они сохраняют всё, что явно не меняют.
Обобщённый гомоморфный сопоставленный тип, применённый к массиву или кортежу, даёт массив или кортеж, а не объект с числовыми ключами. Readonly<string[]> это readonly string[], а при type Box<T> = { [K in keyof T]: { value: T[K] } } Box<[string, number]> это [{ value: string }, { value: number }].
Сопоставленные типы и код времени выполнения
Поскольку сопоставленный тип только описывает объект, функции, которая строит такой объект, нужен собственный цикл времени выполнения. Тогда тип описывает её результат:
Колбэки получают типы параметров из сопоставленного типа (v в одном это string, а в другом number) без всяких аннотаций. Если вы пришли сюда за классом Map, а не за сопоставлением типов, см. Map в TypeScript.
Часто задаваемые вопросы
Что такое сопоставленный тип в TypeScript?
Тип, который создаёт объектный тип, перебирая объединение ключей: { [K in Keys]: SomeType }. Чаще всего ключи это keyof T, поэтому у нового типа те же ключи, что у T, с преобразованными типами свойств, как в type Flags<T> = { [K in keyof T]: boolean }.
Что означают +readonly, -readonly, +? и -? в сопоставленном типе?
Они добавляют или убирают модификаторы. readonly или +readonly делает каждое свойство readonly, -readonly убирает readonly. ? или +? делает каждое свойство необязательным, -? делает их обязательными и также убирает undefined из их типов. Required<T> написан через -?.
Как переименовать ключи в сопоставленном типе?
Используйте конструкцию as после ключа: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } превращает name в getName. Если в конструкции as сопоставить ключ с never, он удаляется; так фильтруют свойства.
Сопоставленный тип это то же самое, что Map в TypeScript?
Нет. Сопоставленный тип это преобразование типов при компиляции, и он не порождает кода. Map<K, V> это класс Map из JavaScript, коллекция пар ключ-значение во время выполнения. Запрос «typescript map type» часто означает то одно, то другое.
Как реализованы Partial и Readonly?
Как сопоставленные типы в стандартной библиотеке: type Partial<T> = { [P in keyof T]?: T[P] } и type Readonly<T> = { readonly [P in keyof T]: T[P] }. Pick и Record тоже сопоставленные типы; Omit это Pick в сочетании с Exclude.