Сигнатуры индекса в 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>;
Заключение
Используйте сигнатуры индекса, когда ключи динамические, но структура значений постоянна.
Сочетайте с явными свойствами, где это возможно, а для продвинутых трансформаций см. Преобразованные типы и Служебные типы.