Ошибка TypeScript: индексный параметр не может быть объединением (union) типов
Понимание проблемы: почему TypeScript запрещает union-типы в индексных сигнатурах
Ошибка возникает из-за фундаментального ограничения индексных сигнатур в TypeScript. Индексная сигнатура вида { [key: string]: T } означает, что любой ключ типа string может существовать в объекте. Когда вы пишете { [key: Role]: UserData }, TypeScript не может гарантировать, что объект будет содержать только ключи 'user' и 'admin' — индексная сигнатура подразумевает неограниченный набор ключей данного типа. Union-тип 'user' | 'admin' — это подмножество string, но сигнатура всё равно разрешает любые строки.
Решение: Mapped Object Types (маппированные типы)
TypeScript предлагает элегантное решение — маппированные типы. Они позволяют точно указать, какие ключи должны быть в объекте и какие типы значений им соответствуют. Вот правильный код:
type Role = 'user' | 'admin';
interface UserData {
name: string;
age: number;
}
// Используем mapped object type
type UsersMap = { [K in Role]: UserData };
// Создаём объект строго с двумя ключами
const users: UsersMap = {
user: { name: 'Alice', age: 30 },
admin: { name: 'Bob', age: 25 }
};
// TypeScript теперь знает все ключи:
users.user // ✅ OK
users.admin // ✅ OK
// users.guest // ❌ Ошибка: Property 'guest' does not exist
Как это работает?
[K in Role]— перебирает каждый элемент union-типаRole('user'и'admin')- Для каждого
Kсоздаётся свойство с типомUserData - TypeScript точно знает, что в объекте обязательно должны быть оба ключа, и никаких других
Альтернатива: Record с частичными ключами
Если вам нужен объект, где не все ключи обязательны (например, может отсутствовать 'admin'), используйте Partial:
type PartialUsersMap = Partial<Record<Role, UserData>>;
// или вручную:
type OptionalUsersMap = { [K in Role]?: UserData };
const optionalUsers: OptionalUsersMap = {
user: { name: 'Charlie', age: 28 }
// admin — опционально, ошибки не будет
};
Почему Record тоже не работал?
Record<Role, UserData> — это просто синтаксический сахар для маппированного типа. Если вы получили ошибку с Record, возможно, вы использовали его в контексте индексной сигнатуры (например, const users: Record<Role, UserData> = {} без указания всех ключей). TypeScript требует, чтобы все ключи из Role были объявлены.
Важное замечание: динамические ключи
Если вы заранее не знаете, какие именно ключи будут (например, получаете их из API), то маппированные типы не подходят — они требуют статического набора ключей. В таких случаях используйте обычную индексную сигнатуру с string и проверяйте ключи во время выполнения:
interface UsersDict {
[key: string]: UserData | undefined;
}
Надеюсь, это объяснение помогло! Маппированные типы — мощный инструмент TypeScript для точной типизации объектов с известным набором ключей.
<<Дополнительный подход: использование Enum вместо union-типа
Если вам не нравится синтаксис маппированных типов, можно использовать enum для определения ролей. Это особенно полезно, когда набор ключей может расширяться:
enum Role {
User = 'user',
Admin = 'admin'
}
interface UserData {
name: string;
age: number;
}
// Маппированный тип с enum
type UsersMap = { [K in Role]: UserData };
const users: UsersMap = {
[Role.User]: { name: 'Alice', age: 30 },
[Role.Admin]: { name: 'Bob', age: 25 }
};
Когда это удобно?
- Вы можете добавить новую роль, просто расширив enum
- Значения enum автоматически становятся строковыми литералами
- Лучшая поддержка рефакторинга в IDE
Обработка отсутствующих ключей
Если вы хотите, чтобы объект мог быть неполным, используйте Partial:
type PartialUsersMap = Partial<{ [K in Role]: UserData }>;
const partialUsers: PartialUsersMap = {
[Role.User]: { name: 'Dave', age: 35 }
};
Этот подход сочетает строгость типов с гибкостью, которую даёт enum. Однако помните: маппированные типы — основной способ работы с фиксированными наборами ключей в TypeScript, и их стоит изучить в первую очередь.