Как задать тип для всех элементов объекта (индексных членов) в TypeScript?
Индексные сигнатуры: базовый подход
То, что вы написали — { [key: string]: User } — это индексная сигнатура. Она говорит TypeScript: «Этот объект может иметь любое количество строковых ключей, и все значения должны быть типа User». Это абсолютно корректный и часто используемый способ.
Проблема с таким подходом — он слишком «вольный». Вы можете случайно обратиться к несуществующему ключу, и TypeScript не выдаст ошибку:
let users: { [key: string]: User } = {};
const user = users["nonexistent"]; // Тип User, но на самом деле undefined!
Использование Record<K, V>
TypeScript предоставляет встроенный тип Record<Keys, Type>. Это синтаксический сахар над индексной сигнатурой. Ваш пример можно переписать так:
let users: Record<string, User> = {};
users["user1"] = { name: "Alice", age: 30 }; // Работает
users["user2"] = { name: "Bob", age: "25" }; // Ошибка: Type 'string' is not assignable to type 'number'
Ограничение ключей (Уточнённые типы ключей)
Если вы хотите разрешить только определённые ключи, используйте union тип для ключей:
type Role = "admin" | "user" | "guest";
// Вариант 1: Record с union типом
let roles: Record<Role, User> = {
admin: { name: "Admin", age: 40 },
user: { name: "User", age: 25 },
guest: { name: "Guest", age: 20 }
};
// Вариант 2: Индексная сигнатура с литеральным типом
let roles2: { [key in Role]: User } = {
admin: { name: "Admin", age: 40 },
user: { name: "User", age: 25 },
guest: { name: "Guest", age: 20 }
};
В обоих случаях вы обязаны указать все ключи из union-типа. Если пропустить хотя бы один — TypeScript выдаст ошибку.
Дополнительная безопасность: Map и Partial
Если объект может содержать не все ключи (опциональные), используйте Partial<Record<K, V>>:
type Role = "admin" | "user" | "guest";
let optionalRoles: Partial<Record<Role, User>> = {
admin: { name: "Admin", age: 40 }
// user и guest необязательны
};
Продвинутый приём: строгая проверка на undefined
Чтобы избежать случайного обращения к несуществующему ключу, используйте Map (нативный или типизированный):
// Строгая типизация с Map
const usersMap = new Map<string, User>();
usersMap.set("user1", { name: "Alice", age: 30 });
const user = usersMap.get("nonexistent"); // Тип User | undefined — ошибка без проверки
if (user) {
console.log(user.name); // Безопасно
}
Итоговые рекомендации
- Для простых случаев с произвольными строковыми ключами —
Record<string, T>или{ [key: string]: T } - Для ограниченного набора ключей —
Record<UnionType, T>с union-типом - Для опциональных ключей —
Partial<Record<UnionType, T>> - Для максимальной безопасности (защита от undefined) —
Map<KeyType, T>
Помните: индексные сигнатуры и Record — это мощные инструменты, но они не проверяют наличие конкретных ключей во время выполнения. Если вам нужна runtime-валидация, используйте библиотеки вроде zod или io-ts.
Альтернативный подход: использование классов и геттеров/сеттеров
Если вам нужен не просто тип, а ещё и контроль над тем, что попадает в объект (например, валидация при добавлении), можно создать класс-обёртку:
class UserRegistry {
private users: Map<string, User> = new Map();
addUser(id: string, user: User): void {
if (this.users.has(id)) {
throw new Error(`User with id ${id} already exists`);
}
this.users.set(id, user);
}
getUser(id: string): User | undefined {
return this.users.get(id);
}
getAllUsers(): User[] {
return Array.from(this.users.values());
}
}
// Использование
const registry = new UserRegistry();
registry.addUser("user1", { name: "Alice", age: 30 });
registry.addUser("user1", { name: "Bob", age: 25 }); // Ошибка в runtime
Этот подход даёт:
- Чёткий API — нельзя случайно перезаписать или удалить элемент
- Runtime-валидацию — можно добавить проверки (например, уникальность ID)
- Инкапсуляцию — внутренняя структура данных скрыта
Недостаток — больше кода и потеря гибкости, если объект нужно сериализовать в JSON или передавать как plain object. В таких случаях лучше оставить Record или индексную сигнатуру.
Выбирайте подход в зависимости от задачи: для простого хранения данных — Record, для сложной бизнес-логики — класс.