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

Сигнатуры индекса в TypeScript дают мощный инструмент для определения типов объектов с динамическими именами свойств — при этом сохраняется типобезопасность.

Они позволяют задать типы значений, к которым можно обращаться через нотацию в квадратных скобках (obj[key]), даже если точные имена свойств заранее неизвестны.


Ключевые концепции

  • Динамический доступ к свойствам: работа с объектами, имеющими произвольные имена свойств.
  • Типобезопасность: гарантия единообразия типов значений для всех динамических свойств.
  • Гибкие структуры данных: моделирование словарей, карт и прочих динамических структур.
  • Безопасность во время выполнения: выявление ошибок, связанных с типами, на этапе компиляции.

Базовые сигнатуры индекса

Строковые сигнатуры индекса

Сигнатуры индекса в TypeScript позволяют определять типы для объектов, где имена свойств неизвестны заранее, но известна структура значений.

Сигнатура индекса задаёт типы для свойств, к которым обращаются через индекс (например, obj[key]).

Пример


// Этот интерфейс описывает объект со строковыми ключами и строковыми значениями
interface StringDictionary {
  [key: string]: string;
}

// Создание объекта, соответствующего интерфейсу
const names: StringDictionary = {
  firstName: "Alice",
  lastName: "Smith",
  "100": "One Hundred"
};

// Обращение к свойствам
console.log(names["firstName"]); // "Alice"
console.log(names["lastName"]);  // "Smith"
console.log(names["100"]);      // "One Hundred"

// Динамическое добавление новых свойств
names["age"] = "30";

// Это вызовет ошибку
// names["age"] = 30; // Ошибка: тип 'number' нельзя присвоить типу 'string'

Синтаксис сигнатуры индекса использует квадратные скобки [key: тип] для описания типов имён свойств (или ключей), которые разрешены, а затем указывает тип значений, которые могут иметь эти свойства.


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

TypeScript поддерживает как строковые, так и числовые сигнатуры индекса.

Пример


// Объект с числовыми индексами
interface NumberDictionary {
  [index: number]: any;
}

const scores: NumberDictionary = {
  0: "Zero",
  1: 100,
  2: true
};

console.log(scores[0]); // "Zero"
console.log(scores[1]); // 100
console.log(scores[2]); // true

// Добавление сложного объекта
scores[3] = { passed: true };

Примечание: В JavaScript все ключи объектов хранятся как строки — даже числовые.

Однако TypeScript проводит различие, чтобы помочь выявлять логические ошибки при работе с массивами и объектами.


Продвинутые шаблоны сигнатур индекса

Смешанные типы свойств

Можно сочетать сигнатуры индекса с явными объявлениями свойств.

Пример


interface UserInfo {
  name: string; // Обязательное свойство с конкретным именем
  age: number;  // Обязательное свойство с конкретным именем
  [key: string]: string | number; // Все остальные свойства должны быть строкой или числом
}

const user: UserInfo = {
  name: "Alice", // Обязательное
  age: 30,      // Обязательное
  address: "123 Main St", // Опциональное
  zipCode: 12345 // Опциональное
};

// Это вызовет ошибку
// const invalidUser: UserInfo = {
//  name: "Bob",
//  age: "thirty", // Ошибка: тип 'string' нельзя присвоить типу 'number'
//  isAdmin: true  // Ошибка: тип 'boolean' нельзя присвоить типу 'string | number'
// };

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

Сигнатуры индекса только для чтения

Можно сделать сигнатуры индекса доступными только для чтения — чтобы запретить изменение после создания.

Пример


interface ReadOnlyStringArray {
  readonly [index: number]: string;
}

const names: ReadOnlyStringArray = ["Alice", "Bob", "Charlie"];

console.log(names[0]); // "Alice"

// Это вызовет ошибку
// names[0] = "Andrew"; // Ошибка: сигнатура индекса в типе 'ReadOnlyStringArray' разрешает только чтение

Для ограничения наборов ключей и преобразования структур см. Преобразованные типы.


Примеры из реальной практики

Обработка ответов API

Пример


// Тип для ответов API с динамическими ключами
interface ApiResponse<T> {
  data: {
    [resourceType: string]: T[];  // напр., { "users": User[], "posts": Post[] }
  };
  meta: {
    page: number;
    total: number;
    [key: string]: any;  // Разрешить дополнительные метаданные
  };
}

// Пример использования с API пользователей
interface User {
  id: number;
  name: string;
  email: string;
}

// Имитация ответа API
const apiResponse: ApiResponse<User> = {
  data: {
    users: [
      { id: 1, name: "Alice", email: "alice@example.com" },
      { id: 2, name: "Bob", email: "bob@example.com" }
    ]
  },
  meta: {
    page: 1,
    total: 2,
    timestamp: "2023-01-01T00:00:00Z"
  }
};

// Обращение к данным
const users = apiResponse.data.users;
console.log(users[0].name); // "Alice"


Лучшие практики

Что следует и не следует делать

Да:

  • Используйте сигнатуры индекса для коллекций с динамическими ключами;
  • Сочетайте с явными свойствами для известных полей;
  • Старайтесь использовать конкретные типы значений (избегайте тип any);
  • Используйте модификатор readonly, если изменение не требуется.

Нет:

  • Не используйте фиксированные интерфейсы, когда ключи известны;
  • Не забывайте, что все свойства должны соответствовать типу сигнатуры индекса;
  • Не изобретайте заново преобразованные типы — используйте специальную страницу для преобразований.

Распространённые ошибки

Конфликты имён свойств


interface ConflictingTypes {
  [key: string]: number;
  name: string; // Ошибка: не совместим с типом строкового индекса 'number'
}

interface FixedTypes {
  [key: string]: number | string;
  name: string;  // OK
  age: number;   // OK
}


Сигнатуры индекса vs. Record<K, T>

Используйте сигнатуру индекса, если нужны гибкие/динамические ключи в сочетании с другими свойствами.

Используйте Record<K, T>, если требуется лаконичное простое преобразование.

Пример


// Сигнатура индекса
interface StringMap {
  [key: string]: string;
}

// Record
type StringRecord = Record<string, string>;


Заключение

Используйте сигнатуры индекса, когда ключи динамические, но структура значений постоянна.

Сочетайте с явными свойствами, где это возможно, а для продвинутых трансформаций см. Преобразованные типы и Служебные типы.