Индексные сигнатуры и вложенные структуры

Типизируем объекты-словари с заранее неизвестными ключами и сложные вложенные структуры.

Индексная сигнатура описывает объект, у которого ключи заранее неизвестны, но известен их тип и тип значений: { [key: string]: number }.

Проблема: ключи неизвестны заранее

interface с фиксированными свойствами не подходит, когда объект используется как словарь — например, «название города → население». Имена ключей появляются динамически. Для этого есть индексная сигнатура:

interface Population {
  [city: string]: number;
}

const data: Population = {
  "Москва": 13_000_000,
  "Казань": 1_300_000,
};

data["Уфа"] = 1_100_000;     // ок
data["Сочи"] = "много";      // Ошибка: значение должно быть number

Запись [city: string]: number читается как «любой строковый ключ ведёт к числу». Имя city здесь только для читаемости.

Где это применяют

Индексные сигнатуры описывают словари, кэши, счётчики, объекты-конфиги — всё, где ключи задаются данными, а не известны заранее. Реальный пример — подсчёт частоты слов:

const text = "кот кот пёс кот пёс";
const counts = {};

for (const word of text.split(" ")) {
  counts[word] = (counts[word] || 0) + 1;
}

for (const word in counts) {
  console.log(word + ": " + counts[word]);
}

Вывод:

кот: 3
пёс: 2

В TypeScript объект counts описали бы как { [word: string]: number } — и компилятор гарантировал бы, что значения всегда числа.

Сочетание с фиксированными свойствами

Можно совместить известные поля и индексную сигнатуру. Но тогда тип фиксированных свойств обязан быть совместим с типом значений сигнатуры:

interface Config {
  version: string;
  [key: string]: string; // все остальные ключи — тоже строки
}

Вложенные структуры

Реальные данные часто вложены: объект внутри объекта, массив объектов. Типы вкладываются так же, как сами данные:

interface Address {
  city: string;
  zip: string;
}

interface User {
  name: string;
  address: Address;        // вложенный объект
  hobbies: string[];       // массив строк
  friends: User[];         // массив таких же объектов
}

const u: User = {
  name: "Аня",
  address: { city: "Казань", zip: "420000" },
  hobbies: ["чтение", "бег"],
  friends: [],
};

Разбивать сложную структуру на отдельные интерфейсы (Address, User) — хорошая практика: каждый кусок переиспользуется и читается понятнее, чем один гигантский вложенный тип.

Доступ к вложенным полям

При обращении к глубоким полям редактор подсказывает на каждом шаге, а компилятор ловит опечатки на любом уровне вложенности:

console.log(u.address.city);  // "Казань"
console.log(u.address.country); // Ошибка: Property 'country' does not exist on type 'Address'.

Итог

  • Индексная сигнатура { [k: string]: T } типизирует объекты-словари с динамическими ключами.
  • Её применяют для кэшей, счётчиков, конфигов — там, где ключи приходят из данных.
  • Вложенные структуры описывают отдельными интерфейсами; компилятор проверяет поля на любой глубине.
Проверьте себя
1. Для чего нужна индексная сигнатура { [key: string]: number }?
AЧтобы запретить добавлять свойства
BЧтобы типизировать объект-словарь с заранее неизвестными строковыми ключами и числовыми значениями
CЧтобы создать массив чисел
DЧтобы сделать все свойства необязательными
2. Почему сложную структуру данных лучше разбивать на отдельные интерфейсы?
AЭто обязательное требование компилятора
BЭто ускоряет программу
CКаждый интерфейс переиспользуется и код читается понятнее, чем один огромный вложенный тип
DВложенные типы вообще не поддерживаются
3. Что покажет компилятор при обращении u.address.country, если в Address нет поля country?
AВернёт undefined без ошибки
BАвтоматически добавит поле country
CНичего, проверка работает только на верхнем уровне
DОшибку: свойство country не существует в типе Address