Avatar ·

TypeScript: как правильно перебрать словарь (объект) в цикле?

Всем привет! Я только начал изучать TypeScript и столкнулся с задачей, которая кажется простой, но вызывает у меня ступор. У меня есть объект, который я использую как словарь (ключи — строки, значения — числа). Мне нужно перебрать все его пары ключ-значение в цикле. В обычном JavaScript я бы сделал так: ```typescript const dict = { apple: 5, banana: 3, orange: 7 }; for (let key in dict) { console.log(key, dict[key]); } ``` Но TypeScript ругается на `dict[key]` — пишет что-то про `Element implicitly has an 'any' type because index expression is not of type 'number'`. Я попробовал использовать `Object.keys`: ```typescript Object.keys(dict).forEach(key => { console.log(key, dict[key]); // опять ошибка! }); ``` Тоже ошибка! Как правильно перебрать объект-словарь в TypeScript? Мне нужно, чтобы TypeScript понимал типы ключей и значений, а не просто ставил `any`. И ещё: какой способ самый производительный и типовой для такой задачи? Заранее спасибо за помощь! <<>>

Проблема с типами при итерации по объекту

Ошибка, которую вы видите, возникает из-за того, что Object.keys() возвращает string[], а не массив конкретных ключей вашего объекта. TypeScript не может гарантировать, что произвольная строка будет существовать как ключ в объекте, поэтому при обращении dict[key] он видит несоответствие типов. Давайте разберём правильные решения.

Решение 1: Использование keyof typeof (рекомендуемый способ)

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

const dict: Record<string, number> = {
  apple: 5,
  banana: 3,
  orange: 7
};

// Вариант A: for...in с приведением типа
for (const key in dict) {
  if (Object.prototype.hasOwnProperty.call(dict, key)) {
    const typedKey = key as keyof typeof dict;
    console.log(typedKey, dict[typedKey]);
  }
}

// Вариант B: Object.keys с приведением
(Object.keys(dict) as Array<keyof typeof dict>).forEach(key => {
  console.log(key, dict[key]);
});

Решение 2: Использование Object.entries() (чище и безопаснее)

Лучший способ для итерации по парам ключ-значение — Object.entries(), который возвращает кортежи [string, T]:

const dict: Record<string, number> = {
  apple: 5,
  banana: 3,
  orange: 7
};

// Просто и элегантно
for (const [key, value] of Object.entries(dict)) {
  console.log(key, value); // key: string, value: number
}

// Если нужен строгий тип ключа
type DictKeys = keyof typeof dict;
for (const [key, value] of Object.entries(dict) as Array<[DictKeys, number]>) {
  console.log(key, value);
}

Решение 3: Специализированные утилиты (для сложных случаев)

Если вы часто работаете со словарями, создайте вспомогательную функцию:

function forEachDict<T extends Record<string, unknown>>(
  dict: T,
  callback: (key: keyof T, value: T[keyof T]) => void
): void {
  for (const key in dict) {
    if (Object.prototype.hasOwnProperty.call(dict, key)) {
      callback(key, dict[key]);
    }
  }
}

// Использование
forEachDict(dict, (key, value) => {
  console.log(key, value); // key: "apple" | "banana" | "orange", value: number
});

Что выбрать?

  • Для простых итераций — Object.entries() с деструктуризацией (Решение 2). Это самый читаемый и безопасный вариант.
  • Для максимальной производительности — for...in с проверкой hasOwnProperty (Решение 1). Он быстрее, чем Object.entries.
  • Для кода с частым перебором словарей — создайте обобщённую функцию (Решение 3).

Важные замечания

  • for...in перебирает все перечисляемые свойства, включая унаследованные. Всегда используйте hasOwnProperty для фильтрации.
  • TypeScript 5.1+ улучшил вывод типов для Object.entries(), но всё равно может потребоваться явное приведение для специфических ключей.
  • Если вы используете Map вместо объекта, то у него есть встроенный метод forEach с правильной типизацией.
<<>>

Альтернативный подход: использование дженериков и Map

Если ваша основная задача — работа со словарём, рекомендую рассмотреть Map вместо обычного объекта. Это даёт встроенную типизацию и удобные методы:

const dict = new Map<string, number>([
  ['apple', 5],
  ['banana', 3],
  ['orange', 7]
]);

// Итерация с правильными типами
dict.forEach((value, key) => {
  console.log(key, value); // key: string, value: number
});

// Или через for...of
for (const [key, value] of dict) {
  console.log(key, value);
}

Map решает проблему типов автоматически: ключи и значения строго типизированы, нет необходимости в приведении. Кроме того, Map сохраняет порядок вставки элементов и имеет константную сложность операций вставки/удаления.

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

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

Для миграции с объекта на Map можно использовать Object.entries():

const obj = { apple: 5, banana: 3 };
const map = new Map(Object.entries(obj));

Этот подход полностью решает проблему типов и делает код более идиоматичным для TypeScript.