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

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

Индексная сигнатура описывает объект, у которого ключи заранее неизвестны, но известен их тип и тип значений: { [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 не существует в типе Address
CАвтоматически добавит поле country
DНичего, проверка работает только на верхнем уровне
Поддержать проект