Menu

Record в TypeScript: тип Record<K, V> с примерами

Record<K, V> это объектный тип, ключи которого имеют тип K, а все значения тип V. Record со строковыми ключами и с объединением ключей (каждый ключ обязателен), Partial<Record>, перебор Record с типизированными ключами, ловушка отсутствующего ключа и когда вместо него использовать сигнатуру индекса или Map.

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

Record<K, V> это встроенный служебный тип для объекта, ключи которого имеют тип K, а все значения тип V. Со строковыми ключами string он описывает словарь; с объединением литеральных ключей он описывает объект, который должен иметь ровно эти ключи.

Record существует только в системе типов. Во время выполнения оба объекта это обычные объекты JavaScript, поэтому они работают с объектными литералами, развёртыванием, JSON.stringify и всем остальным, что принимает объект.

Синтаксис и определение

Record<Keys, Value>

Keys должен быть тем, что может служить ключом объекта: string, number, symbol, объединение строковых или числовых литералов или шаблонный литеральный тип. Value может быть любым типом. Всё определение в стандартной библиотеке TypeScript занимает одну строку, это сопоставленный тип:

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

keyof any это string | number | symbol, множество всех возможных типов ключей. [P in K]: T создаёт по одному свойству типа T для каждого члена K. Этим объясняются оба поведения ниже: широкий K вроде string даёт сигнатуру индекса (любой ключ), а объединение в K даёт по одному обязательному свойству на каждый член.

Объединение ключей: каждый ключ обязателен

Когда ключи это объединение литералов, Record должен перечислить каждый из них и никаких других. Это превращает компилятор в чек-лист:

Добавьте "cancelled" в Status, и оба объекта перестанут компилироваться, пока вы не зададите новому статусу подпись и цвет. Пропущенный ключ или ключ, которого нет в объединении, это ошибка компиляции:

index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.

То же работает со строковым enum в роли типа ключа: Record<Color, string> требует по записи на каждый член enum.

Record<string, T> и отсутствующий ключ

Со строковыми ключами string допустим любой ключ, и TypeScript типизирует каждое чтение как T, даже для ключа, которого нет. Во время выполнения отсутствующий ключ даёт undefined:

Это самая частая ошибка с Record. Есть три способа с ней справиться: проверять через in или Object.hasOwn перед чтением, объявить значение как V | undefined или включить параметр компилятора noUncheckedIndexedAccess, который добавляет | undefined к каждому чтению по сигнатуре индекса во всём проекте. У Record с объединением ключей такой проблемы нет, потому что каждый ключ гарантированно существует.

Partial<Record<K, V>>: только часть ключей

Чтобы использовать объединение ключей, но не требовать их все, оберните Record в Partial. Тогда чтения возвращают V | undefined, и это честно:

Ключ с опечаткой вроде jp по-прежнему ошибка, и в этом преимущество перед Record<string, string>.

Перебор Record

Object.keys, Object.values и Object.entries работают. Подвох в типе ключа: Object.keys возвращает string[], а Object.entries возвращает [string, V][], но никогда не ваше объединение ключей:

TypeScript намеренно оставляет ключи как string: во время выполнения у объекта может быть больше свойств, чем перечисляет его тип, поэтому в общем случае обещать Plan[] небезопасно. Для объекта, созданного из литерала, как seats, приведение безопасно.

Построение Record из данных

Record это обычный результат группировки или индексации массива. Начните с пустого объекта с типом Record и заполните его:

Book["genre"] использует объединение из интерфейса как тип ключа, поэтому добавление жанра в Book заставит byGenre требовать новую запись.

Record<string, unknown> и интерфейсы

Record<string, unknown> это распространённый тип для «какого-то объекта со строковыми ключами». Он принимает объектные литералы и значения, типизированные псевдонимом type, а интерфейс отвергает:

index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
  Index signature for type 'string' is missing in type 'User'.

Интерфейсы можно расширять слиянием объявлений, поэтому TypeScript не предполагает, что они подходят под сигнатуру индекса; псевдонимы типов нельзя открыть заново, поэтому type User = { name: string } прошёл бы. Обычные исправления: принимать object (на нём всё равно можно вызвать Object.keys), сделать функцию обобщённой (<T extends object>(obj: T)) или объявить User через type (другие различия описаны на странице interface или type).

Record, сигнатура индекса или Map

Record<K, V>{ [key: string]: V }Map<K, V>
Существует во время выполнениянет, обычный объектнет, обычный объектда, класс
Фиксированный набор ключейда, при объединении в Kнетнет
Типы ключейstring, number, symbol, объединения литералов, шаблоныstring, number, symbol, шаблонылюбые, включая объекты
Тип при чтении отсутствующего ключаV (при ключах string)VV | undefined из get
Сочетание с именованными свойствамичерез пересечение &да, в том же типенет
JSON и развёртываниедаданет, сначала преобразовать
РазмерObject.keys(r).lengthObject.keys(o).lengthm.size
Частые добавления и удаленияработаетработаетсоздан для этого

Выбирайте Record с объединением ключей всегда, когда набор ключей известен: это единственный вариант, который проверяет наличие каждого ключа. Для открытого набора строковых ключей Record<string, V> и сигнатура индекса взаимозаменяемы, и многие кодовые базы предпочитают Record ради читаемости. Берите Map, когда ключи добавляются и удаляются во время выполнения, когда ключи не строки или когда без лишней работы нужны размер и порядок вставки.

Часто задаваемые вопросы

Что такое Record в TypeScript?

Record<K, V> это встроенный служебный тип для объекта, ключи которого имеют тип K, а все значения тип V. Record<string, number> это объект с любыми строковыми ключами и числовыми значениями; Record<"en" | "de", string> это объект ровно с ключами en и de, оба строки.

Чем Record отличается от Map в TypeScript?

Record это тип для обычного объекта JavaScript, поэтому он работает с объектными литералами, JSON и развёртыванием и исчезает при компиляции. Map это класс времени выполнения с get, set, has и size, он сохраняет порядок вставки для всех ключей, принимает ключи любого типа (в том числе объекты), а get возвращает V | undefined. Используйте Record для фиксированных данных или данных в форме JSON, а Map для ключей, которые добавляются и удаляются во время выполнения.

Чем Record<string, T> отличается от { [key: string]: T }?

Для значений это один и тот же тип: Record<string, T> разворачивается в объектный тип со строковой сигнатурой индекса. Два небольших различия: у сигнатуры индекса может быть имя, и она может соседствовать с другими свойствами в том же типе, а keyof Record<string, T> это string, тогда как keyof { [key: string]: T } это string | number.

Как перебрать Record в TypeScript?

Используйте Object.entries(record) для пар ключ-значение, Object.keys для ключей и Object.values для значений. Ключи возвращаются как string, а не как K, потому что во время выполнения у объекта могут быть лишние ключи. Когда у Record объединение известных ключей, используйте приведение: (Object.keys(r) as Array<keyof typeof r>).

Как сделать обязательными только некоторые ключи Record?

С объединением ключей Record<K, V> требует каждый ключ. Оберните его в Partial, чтобы сделать все необязательными: Partial<Record<Lang, string>>. Для смешанного варианта используйте пересечение: Record<"en", string> & Partial<Record<"de" | "fr", string>> требует en и допускает остальные.

Coddy programming languages illustration

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

НАЧАТЬ