В 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) !== undefined | Map | Та же оговорка про 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 эту опцию не включает.