Menu

Словарь в TypeScript: индексные сигнатуры, Record и Map

В TypeScript нет класса словаря или хеш-таблицы; поиск по ключу типизируют индексной сигнатурой, Record<K, V> или Map<K, V>. Разберём каждый вариант, проверку наличия ключа, добавление, удаление и перебор и зачем словарям noUncheckedIndexedAccess.

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

В TypeScript нет отдельного класса словаря или хеш-таблицы. Словарь это либо обычный объект с индексной сигнатурой, { [key: string]: number }, либо тот же тип в записи Record<string, number>, либо Map<string, number>. Все три хранят значения по ключу; различаются они типами ключей, типизацией отсутствующих ключей и сериализацией.

Для строковых ключей и данных в форме JSON обычно выбирают объект с Record<string, T>. Для нестроковых ключей или записей, которые постоянно добавляются и удаляются, используйте Map.

Индексные сигнатуры

Индексная сигнатура, [key: KeyType]: ValueType, говорит: «любой ключ этого типа отображается в значение этого типа». Имя ключа (key, name, userId) служит только документацией. Типом ключа может быть string, number, symbol, шаблон шаблонного литерала или их объединение.

JavaScript превращает числовые ключи в строки, поэтому у объекта { [id: number]: string } во время выполнения всё равно строковые ключи: Object.keys({ 1: "one" }) даёт [ '1' ]. Числовая индексная сигнатура лишь ограничивает, как его можно индексировать в TypeScript.

Record<K, V>

Record<string, V> это сокращение для { [key: string]: V }. Если вместо string взять объединение литеральных ключей, получится фиксированный словарь, обязанный содержать каждый ключ:

Пропуск staging в urls даёт ошибку компиляции TS2741 (Property 'staging' is missing...), что превращает Record с ключами-объединением в проверяемую таблицу поиска. Partial делает каждое значение number | undefined.

Map как хеш-таблица

Map принимает ключи любого типа, сохраняет порядок вставки, имеет size и честно типизирует отсутствующие ключи: get возвращает V | undefined.

Проверка наличия ключа

Проверок несколько, и означают они не одно и то же:

ПроверкаРаботает дляНа что обратить внимание
Object.hasOwn(obj, key)объектовES2022; на старых target используйте Object.prototype.hasOwnProperty.call(obj, key)
key in objобъектовИстинно и для унаследованных ключей вроде toString и constructor
obj[key] !== undefinedобъектовНе отличает отсутствующий ключ от ключа со значением undefined
if (obj[key])объектовЛожно и для значений 0, "" и false
map.has(key)MapНе сужает последующий map.get(key)
map.get(key) !== undefinedMapТа же оговорка про undefined, что и у объектов

Из-за проблемы с унаследованными ключами обычные объекты рискованно использовать как словари, когда ключи задают пользователи: например, "constructor" или "__proto__". У Map таких ключей нет.

Проблема типа отсутствующего ключа

Для индексной сигнатуры или Record<string, T> чтение любого ключа имеет тип T, даже если ключа не существует. Компилятор позволяет вызывать методы у значения, которое во время выполнения равно undefined:

Опция компилятора noUncheckedIndexedAccess это исправляет: с ней colors["grass"] имеет тип string | undefined, и вызов toUpperCase остаётся ошибкой компиляции, пока вы не сделаете проверку. Она не входит в strict, поэтому её нужно включать отдельно в tsconfig.json; о соседних флагах читайте на странице строгий режим. У Map такой дыры нет, потому что get всегда включает undefined.

Добавление, удаление и перебор

delete работает для свойств индексной сигнатуры. Для обязательного именованного свойства объектного типа он даёт ошибку компиляции TS2790, The operand of a 'delete' operator must be optional.

Что выбрать

ЗадачаИспользовать
Строковые ключи, JSON на входе или выходеRecord<string, T>
Фиксированный известный набор ключей, все обязательныRecord<"a" | "b", T>
Именованные свойства плюс произвольные дополнительные ключиобъектный тип с индексной сигнатурой
Ключи-объекты, числа, которые должны остаться числами, или любые нестроковые ключиMap<K, V>
Частые добавления и удаления или часто читаемый размерMap<K, V>
Ключи, которые приходят от пользователейMap<K, V> (нет унаследованных ключей)

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

Как создать словарь в TypeScript?

Типизируйте обычный объект индексной сигнатурой, const ages: { [name: string]: number } = {}, или равнозначным Record<string, number>. Затем добавляйте записи: ages["ada"] = 36. Для нестроковых ключей или коллекции с частыми добавлениями и удалениями используйте new Map<string, number>().

Есть ли в TypeScript HashMap?

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

Как проверить, есть ли ключ в словаре TypeScript?

Для словаря-объекта используйте Object.hasOwn(dict, key) или key in dict (который видит и унаследованные свойства вроде toString), либо прочитайте значение и сравните его с undefined. Для Map используйте map.has(key) или проверяйте результат map.get(key) напрямую, потому что has не сужает последующий get.

Чем индексная сигнатура отличается от Record?

{ [key: string]: T } и Record<string, T> описывают один и тот же тип. Record короче и может принимать объединение конкретных ключей, Record<"a" | "b", T>, что требует наличия каждого ключа. Индексную сигнатуру можно сочетать с именованными свойствами в одном объектном типе, а её ключу можно дать поясняющее имя, как в { [userId: string]: User }.

Почему чтение отсутствующего ключа словаря не даёт ошибки?

По умолчанию dict[key] для индексной сигнатуры или Record<string, T> имеет тип T, хотя во время выполнения для отсутствующего ключа значение равно undefined. Включите noUncheckedIndexedAccess в tsconfig.json, и тип станет T | undefined, что заставит делать проверку. strict эту опцию не включает.

Coddy programming languages illustration

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

НАЧАТЬ