Перейти к содержимому
Теория · Интерфейсы и структурная типизация · урок 9 из 12

Индексные сигнатуры

Кладовщик ведёт тетрадь учёта. В каждой строке слева название товара, справа — сколько его на складе: «яблоки: 5», «груши: 2». Какие товары привезут завтра, заранее неизвестно, поэтому готовых строк с названиями в тетради нет. Известно одно правило: справа всегда стоит число.

Интерфейс с перечисленными свойствами такую тетрадь не опишет. В interface Stock { apple: number; pear: number } нельзя записать сливы: свойства plum в типе нет. Нужен способ сказать «ключ — любая строка, значение — число». Для этого есть индексная сигнатура:

TypeScript
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

Давайте разберём пример по строкам:

  1. [item: string]: number — у объекта типа Stock может быть сколько угодно свойств со строковыми ключами, и в каждом лежит число.
  2. Литерал задаёт два товара, и оба значения — числа.
  3. stock.plum = 7 и stock["kiwi"] = 1 добавляют новые ключи. tsc не возражает: ключ — строка, значение — число.
  4. Программа печатает 5 7, а затем 4: в объекте четыре ключа.

Как читается сигнатура

Запись [item: string]: number состоит из трёх частей:

  • квадратные скобки означают «любой ключ», а не одно конкретное свойство;
  • item: string — имя ключа и его тип; имя пишут для читателя, на проверку оно не влияет;
  • : number после скобок — тип значения под любым ключом.

Сигнатуру можно написать и прямо в аннотации, без интерфейса: const stock: { [item: string]: number } = {};.

Тип значения tsc проверяет при каждой записи. Строка stock.pear = "two" не пройдёт: ошибка TS2322, строку нельзя записать туда, где ждут число.

Несуществующий ключ

Сигнатура обещает, что под любым ключом лежит число. Но товара может и не быть:

TypeScript
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 честно говорит, что ключа может не быть:

TypeScript
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:

TypeScript
interface Stock {
    [item: string]: number;
}

const stock: Stock = { apple: 5 };
console.log(stock["mango"].toFixed(1)); // ✗
TEXT
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 требует сам.