Индексные сигнатуры в TypeScript
Индексная сигнатура позволяет описать объект, имена свойств которого заранее неизвестны, и при этом указать, какой тип должно иметь каждое значение.
Как устроены индексные сигнатуры в TypeScript
Индексные сигнатуры дают возможность типизировать объекты с динамическими именами свойств, не теряя безопасности типов.
Они сообщают TypeScript, значение какого типа вернётся при чтении свойства через квадратные скобки (obj[key]), даже если точные имена свойств заранее неизвестны.
Главные идеи:
- Динамический доступ к свойствам: работа с объектами, имеющими любые имена свойств.
- Безопасность типов: каждое динамическое свойство должно хранить значение одного и того же вида.
- Гибкие структуры данных: моделирование словарей, отображений и других таблиц поиска.
- Ранние ошибки: ошибки в типах значений обнаруживаются на этапе компиляции.
Базовые индексные сигнатуры
Строковые индексные сигнатуры. Используйте индексную сигнатуру, когда имена свойств заранее неизвестны, но известна форма значений.
В синтаксисе применяются квадратные скобки: [key: type] описывает тип имён свойств (ключей), а после двоеточия указывается тип значений, которые эти свойства могут хранить.
Числовые индексные сигнатуры. TypeScript поддерживает не только строковые, но и числовые ключи.
Примечание: В JavaScript все ключи объекта хранятся как строки, даже числовые. TypeScript всё же различает эти два случая, чтобы помочь обнаружить логические ошибки при путанице между массивами и объектами.
Строковая индексная сигнатура
StringDictionary принимает любой строковый ключ, но каждое значение должно быть типа string. Чтение, добавление и обновление ключей работают, а присваивание числа является ошибкой.
StringDictionary, ключи которого являются строками, а значения — строками.
Создайте объект names этого типа со значениями firstName: "Alice", lastName: "Smith" и ключом "100", сопоставленным со значением "Сто", выведите три значения с доступом через квадратные скобки, затем добавьте ключ age со строкой "30". Оставьте закомментированную строку, показывающую, что присваивание числа 30 было бы ошибкой типа.
Вывод: программа на TypeScript и захваченный вывод
// Этот интерфейс описывает объект со строковыми ключами и строковыми значениями
interface StringDictionary {
[key: string]: string;
}
// Создание соответствующего объекта
const names: StringDictionary = {
firstName: "Alice",
lastName: "Smith",
"100": "Сто"
};
// Доступ к свойствам
console.log(names["firstName"]); // "Alice"
console.log(names["lastName"]); // "Smith"
console.log(names["100"]); // "Сто"
// Динамическое добавление новых свойств
names["age"] = "30";
// Это вызвало бы ошибку
// names["age"] = 30; // Ошибка: тип 'number' не может быть присвоен типу 'string'
Числовая индексная сигнатура
NumberDictionary использует числовые ключи. Поскольку типом значения является any, каждая запись может хранить значение своего вида.
NumberDictionary, ключи которого являются числами, а значения имеют тип any.
Создайте объект scores со значениями 0: "Ноль", 1: 100 и 2: true, выведите три записи по числовому индексу, затем добавьте индекс 3, хранящий объект { passed: true }.
Вывод: программа на TypeScript и захваченный вывод
// Объект с числовыми индексами
interface NumberDictionary {
[index: number]: any;
}
const scores: NumberDictionary = {
0: "Ноль",
1: 100,
2: true
};
console.log(scores[0]); // "Ноль"
console.log(scores[1]); // 100
console.log(scores[2]); // true
// Добавление сложного объекта
scores[3] = { passed: true };
Продвинутые приёмы работы с индексными сигнатурами
Смешанные типы свойств. Индексную сигнатуру можно сочетать с явно названными свойствами. Названные свойства обязательны; всё остальное должно соответствовать индексной сигнатуре.
Важно: Когда вы смешиваете явные свойства с индексной сигнатурой, тип каждого явного свойства должен быть совместим с типом значения индексной сигнатуры.
Индексные сигнатуры только для чтения. Добавьте readonly к индексной сигнатуре, чтобы запретить изменение значений после создания объекта.
Об ограничении наборов ключей и преобразовании форм читайте в уроке о сопоставленных типах (Mapped Types).
Смешанные типы свойств
UserInfo требует наличия name и age и разрешает любое другое свойство при условии, что его значение имеет тип string или number.
UserInfo: обязательные name: string и age: number плюс сигнатура [key: string]: string | number для любого другого свойства.
Создайте корректный объект user со значениями name: "Alice", age: 30, address: "123 Main St" и zipCode: 12345. Добавьте закомментированный invalidUser, который показывает, что age: "thirty" и isAdmin: true каждый были бы ошибкой типа. Файл ничего не выводит.
Вывод: программа на TypeScript и захваченный вывод
interface UserInfo {
name: string; // Обязательное свойство с конкретным именем
age: number; // Обязательное свойство с конкретным именем
[key: string]: string | number; // Все остальные свойства должны быть 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'
// };
Индексная сигнатура только для чтения
Читать names[0] можно, но запись по любому индексу типа ReadOnlyStringArray является ошибкой компиляции.
ReadOnlyStringArray с индексной сигнатурой readonly [index: number]: string.
Присвойте массив ["Alice", "Bob", "Charlie"] переменной names этого типа, выведите names[0] и оставьте закомментированное присваивание в names[0] с пояснением, что запись через индексную сигнатуру только для чтения является ошибкой типа.
Вывод: программа на TypeScript и захваченный вывод
interface ReadOnlyStringArray {
readonly [index: number]: string;
}
const names: ReadOnlyStringArray = ["Alice", "Bob", "Charlie"];
console.log(names[0]); // "Alice"
// Это вызвало бы ошибку
// names[0] = "Andrew"; // Ошибка: индексная сигнатура типа 'ReadOnlyStringArray' допускает только чтение
Примеры из реальной практики
Обработка ответов API. Индексные сигнатуры удобны для ответов API, где имена ресурсов и дополнительные поля метаданных не фиксированы.
Обработка ответа API
ApiResponse<T> позволяет полю data хранить любое имя ресурса, сопоставленное с массивом T, а полю meta — нести дополнительные поля помимо page и total.
ApiResponse<T>, у которого поле data сопоставляет любое имя ресурса ([resourceType: string]) с T[], а поле meta содержит page: number, total: number плюс сигнатуру [key: string]: any для дополнительных метаданных.
Объявите интерфейс User (id, name, email), создайте apiResponse: ApiResponse<User> с двумя пользователями (Alice и Bob) и метаданными page: 1, total: 2, timestamp: "2023-01-01T00:00:00Z", затем выведите имя первого пользователя.
Вывод: программа на TypeScript и захваченный вывод
// Тип для ответов 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, когда значения не должны меняться.
Не стоит делать:
- Браться за индексную сигнатуру, когда ключи известны, — лучше использовать фиксированный интерфейс.
- Забывать, что каждое свойство, включая явные, должно соответствовать типу значения индексной сигнатуры.
- Изобретать заново сопоставленные типы — используйте их для преобразования форм.
Частые ошибки
Распространённая ошибка — объявить явное свойство, тип которого не соответствует индексной сигнатуре. Чтобы это исправить, расширьте тип значения индексной сигнатуры.
Конфликт типов свойства и индекса
ConflictingTypes вызывает ошибку, потому что name: string не соответствует индексной сигнатуре с типом number; FixedTypes расширяет тип значения до number | string, поэтому оба свойства допустимы.
interface ConflictingTypes {
[key: string]: number;
name: string; // Error: not assignable to string index type 'number'
}
interface FixedTypes {
[key: string]: number | string;
name: string; // OK
age: number; // OK
}
Индексные сигнатуры и Record
Используйте индексную сигнатуру, когда ключи гибкие или динамические, а также когда нужно добавить другие свойства.
Используйте Record<K, T> для кратких и простых отображений одного типа ключа в один тип значения.
Индексная сигнатура и Record рядом
StringMap и StringRecord описывают одну и ту же форму — любой строковый ключ, сопоставленный со строковым значением, — записанную двумя разными способами.
// Index signature
interface StringMap {
[key: string]: string;
}
// Record
type StringRecord = Record<string, string>;
Заключение
Используйте индексные сигнатуры, когда ключи динамические, но форма значений постоянна.
По возможности сочетайте их с явными свойствами, а о более сложных преобразованиях читайте в уроках о сопоставленных типах (Mapped Types) и служебных типах (Utility Types).