Avatar ·

Объявление и инициализация словаря (Dictionary) в TypeScript

Привет коллеги! Я новичок в TypeScript (раньше писал на чистом JS) и столкнулся с проблемой при объявлении словаря (Dictionary, он же ассоциативный массив). В JavaScript я делал так: ```typescript let dict = {}; dict["key1"] = "value1"; dict["key2"] = 123; ``` И всё работало. Но в TypeScript мне нужно строго типизировать словарь, где ключи — строки, а значения — числа. Я попробовал несколько вариантов, но везде получаю ошибки компиляции или странное поведение. Вариант 1: ```typescript let dict = {}; dict["apple"] = 5; // Error: Element implicitly has an 'any' type ``` Вариант 2: ```typescript let dict: object = {}; dict["apple"] = 5; // Error: Property 'apple' does not exist on type 'object' ``` Вариант 3: ```typescript let dict: { [key: string]: number } = {}; dict["apple"] = "text"; // Я хочу, чтобы это было ошибкой, но... // На самом деле тут ошибка будет, но не всегда понятно как правильно. ``` Я хочу: 1. Объявить пустой словарь с ключами-строками и значениями-числами. 2. Инициализировать его сразу с какими-то данными. 3. Добавлять новые пары ключ-значение после объявления. 4. Чтобы TypeScript строго проверял типы значений. Подскажите, как правильно это сделать? Есть ли разница между `Map` и обычным объектом? Когда что использовать? Заранее спасибо за помощь! <<>>

Основной способ: индексная сигнатура (Index Signature)

Самый распространённый и правильный способ объявить словарь в TypeScript — использовать индексную сигнатуру. Она говорит компилятору: "этот объект может иметь любое количество свойств с ключами определённого типа, и все значения будут одного типа".

// Пустой словарь
let dict: { [key: string]: number } = {};

// Добавление элементов
dict["apple"] = 5;
dict["banana"] = 10;

// Инициализация с данными
let dict2: { [key: string]: number } = {
    "apple": 5,
    "banana": 10,
    "cherry": 15
};

// Попытка добавить значение другого типа вызовет ошибку
// dict["test"] = "hello"; // Ошибка: Type 'string' is not assignable to type 'number'

// Доступ к значению
console.log(dict["apple"]); // 5

Сокращённая запись: Record<K, V>

TypeScript предоставляет встроенный утилитарный тип Record, который делает то же самое, но короче:

let dict: Record<string, number> = {};
dict["apple"] = 5;

let dict2: Record<string, number> = {
    "apple": 5,
    "banana": 10
};

Record<K, V> — это просто синтаксический сахар для { [key: K]: V }. Используйте его для краткости.

Когда использовать Map вместо объекта?

В TypeScript (и JavaScript) есть встроенный класс Map, который тоже можно использовать как словарь. Вот основные отличия:

  • Ключи: В объекте ключи могут быть только строками или символами. В Map ключом может быть любой тип (объект, число, функция).
  • Порядок: Map гарантирует порядок вставки элементов. Объект (с ES6) тоже сохраняет порядок для строковых ключей, но это менее явно.
  • Производительность: Map оптимизирован для частого добавления/удаления пар ключ-значение.
  • Размер: У Map есть свойство size. У объекта нужно использовать Object.keys(obj).length.
// Map с типизацией
let map: Map<string, number> = new Map();
map.set("apple", 5);
map.set("banana", 10);

// Инициализация с данными
let map2: Map<string, number> = new Map([
    ["apple", 5],
    ["banana", 10]
]);

// Получение значения
console.log(map.get("apple")); // 5

// Проверка наличия ключа
console.log(map.has("apple")); // true

// Удаление
map.delete("apple");

Рекомендация

Используйте объект с индексной сигнатурой (Record), если:

  • Ключи — только строки.
  • Вам нужна простая структура данных без дополнительных методов.
  • Вы работаете с JSON-данными (объекты легко сериализуются).

Используйте Map, если:

  • Ключи — не строки (числа, объекты).
  • Вам часто нужно проверять размер коллекции.
  • Вы активно добавляете/удаляете элементы и важна производительность.

В вашем случае (ключи-строки, значения-числа) идеально подойдёт Record<string, number>.

<<>>

Более строгая типизация с помощью интерфейса

Если вы знаете все возможные ключи словаря заранее, можно создать интерфейс с конкретными свойствами. Это даст ещё более строгую проверку — вы не сможете обратиться к несуществующему ключу без ошибки.

interface FruitPrices {
    apple: number;
    banana: number;
    cherry?: number; // необязательное свойство
}

let dict: FruitPrices = {
    apple: 5,
    banana: 10
};

// Работает
dict["apple"] = 7;
dict.cherry = 15;

// Ошибка! Свойства 'orange' нет в интерфейсе
// dict["orange"] = 20; // Error

// Ошибка! Тип значения не соответствует
// dict["apple"] = "text"; // Error

Этот подход хорош, когда структура словаря фиксирована и известна на этапе разработки. Если ключи динамические (например, приходят из API), используйте Record или индексную сигнатуру.

Предостережение: пустой объект с as

Иногда новички делают так:

let dict = {} as Record<string, number>;
dict["apple"] = 5; // Работает

Это не рекомендуется, потому что as (приведение типа) обходит проверки компилятора. Вы можете случайно присвоить значение неправильного типа, и TypeScript не заметит ошибки до рантайма.

Лучше всегда использовать явную аннотацию типа:

let dict: Record<string, number> = {};