Индексные сигнатуры
Кладовщик ведёт тетрадь учёта. В каждой строке слева название товара, справа — сколько его на складе: «яблоки: 5», «груши: 2». Какие товары привезут завтра, заранее неизвестно, поэтому готовых строк с названиями в тетради нет. Известно одно правило: справа всегда стоит число.
Интерфейс с перечисленными свойствами такую тетрадь не опишет. В interface Stock { apple: number; pear: number } нельзя записать сливы: свойства plum в типе нет. Нужен способ сказать «ключ — любая строка, значение — число». Для этого есть индексная сигнатура:
interface Stock {
[item: string]: number;
}
const stock: Stock = { apple: 5, pear: 2 };
stock.plum = 7;
stock["kiwi"] = 1;
console.log(stock.apple, stock["plum"]); // 5 7
console.log(Object.keys(stock).length); // 4Давайте разберём пример по строкам:
[item: string]: number— у объекта типаStockможет быть сколько угодно свойств со строковыми ключами, и в каждом лежит число.- Литерал задаёт два товара, и оба значения — числа.
stock.plum = 7иstock["kiwi"] = 1добавляют новые ключи. tsc не возражает: ключ — строка, значение — число.- Программа печатает
5 7, а затем4: в объекте четыре ключа.
Как читается сигнатура
Запись [item: string]: number состоит из трёх частей:
- квадратные скобки означают «любой ключ», а не одно конкретное свойство;
item: string— имя ключа и его тип; имя пишут для читателя, на проверку оно не влияет;: numberпосле скобок — тип значения под любым ключом.
Сигнатуру можно написать и прямо в аннотации, без интерфейса: const stock: { [item: string]: number } = {};.
Тип значения tsc проверяет при каждой записи. Строка stock.pear = "two" не пройдёт: ошибка TS2322, строку нельзя записать туда, где ждут число.
Несуществующий ключ
Сигнатура обещает, что под любым ключом лежит число. Но товара может и не быть:
interface Stock {
[item: string]: number;
}
const stock: Stock = { apple: 5 };
const count = stock["mango"];
console.log(count); // undefinedНаведите курсор на count, и редактор покажет тип number, а не number | undefined. Ключа mango в объекте нет, поэтому при запуске в count лежит undefined, и tsc об этом не предупреждает.
Здесь аналогия с тетрадью перестаёт работать. Кладовщик видит, что строки «манго» нет, а TypeScript по сигнатуре считает, что число есть под любым ключом.
У компилятора есть настройка noUncheckedIndexedAccess. С ней чтение по ключу даёт number | undefined, и значение придётся проверить, как необязательное свойство. В строгий режим она не входит, её включают отдельно.
Map вместо объекта
Для словаря в JavaScript есть и Map. Его метод get честно говорит, что ключа может не быть:
const stock = new Map([["apple", 5]]);
const count = stock.get("mango");
console.log(count ?? 0); // 0У stock редактор покажет тип Map<string, number>: ключи — строки, значения — числа. У count тип number | undefined, поэтому без проверки им не воспользоваться, и ?? 0 подставляет ноль. Map удобен, когда ключи добавляют и удаляют по ходу программы. Обычный объект с индексной сигнатурой встречается там, где данные приходят готовым объектом, например из JSON.
Частая ошибка
Ключ прочитали без проверки, поверив типу number:
interface Stock {
[item: string]: number;
}
const stock: Stock = { apple: 5 };
console.log(stock["mango"].toFixed(1)); // ✗TypeError: Cannot read properties of undefined (reading 'toFixed')tsc промолчит: для него stock["mango"] — число. Программа упадёт уже при запуске, потому что в stock["mango"] лежит undefined. Если ключа может не быть, проверьте его, например через in: внутри if ("mango" in stock) значение на месте.
Главное
[item: string]: numberописывает словарь: любой строковый ключ, под каждым — число.- Тип значения проверяется при записи: строку в такой словарь не положить.
- Чтение несуществующего ключа tsc типизирует как
number, хотя при запуске тамundefined; это меняет настройкаnoUncheckedIndexedAccess. - У
Mapметодgetвозвращаетnumber | undefined, и проверку tsc требует сам.