TypeScript: как правильно перебрать словарь (объект) в цикле?
Проблема с типами при итерации по объекту
Ошибка, которую вы видите, возникает из-за того, что 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.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги