Menu

Сопоставленные типы в TypeScript: синтаксис, модификаторы, as

Сопоставленный тип (mapped type) строит новый объектный тип, перебирая ключи: { [K in keyof T]: ... }. Синтаксис, модификаторы readonly и ? с + и -, переназначение ключей через as, фильтрация ключей и как написаны Partial, Readonly, Required, Pick и Record.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Сопоставленный тип строит новый объектный тип, перебирая набор ключей. { [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.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ