Ошибка TypeScript: "An index signature parameter type cannot be a union type" — как исправить?
Почему возникает ошибка и как её исправить с помощью mapped types
Ошибка возникает из-за ограничения TypeScript: в индексных сигнатурах ([key: SomeType]: ValueType) тип ключа может быть только string, number или symbol. Union-типы (например, 'admin' | 'user') не допускаются, так как они не являются конкретными примитивами. Однако ваша задача — не просто разрешить любые строки, а строго ограничить набор возможных ключей. Для этого существуют mapped types (сопоставленные типы).
Mapped types позволяют создать тип на основе другого типа или union-типа. В вашем случае нужно «перебрать» все элементы union UserRole и для каждого создать свойство с типом boolean. Вот правильное решение:
type UserRole = 'admin' | 'user';
type Settings = {
[key in UserRole]: boolean; // Используем mapped type
};
// Теперь объект будет строго типизирован:
const config: Settings = {
admin: true,
user: false,
};
// config.guest = true; // Ошибка: "guest" не существует в типе Settings
Обратите внимание на синтаксис: [key in UserRole]: boolean — это и есть mapped type. Он перебирает все строки из union UserRole и создаёт для каждой ключ с типом boolean. В отличие от индексной сигнатуры ([key: string]: boolean), такой тип не разрешает произвольные ключи — только те, что указаны в union.
Дополнительные возможности mapped types
Вы можете комбинировать mapped types с модификаторами (readonly, optional) и даже с условными типами. Например:
type UserRole = 'admin' | 'user' | 'moderator';
// Сделаем все поля обязательными и доступными только для чтения
type ReadonlySettings = {
readonly [key in UserRole]: boolean;
};
// Или с опциональными полями:
type PartialSettings = {
[key in UserRole]?: boolean;
};
// Или с разными типами значений (используя conditional types):
type RolePermissions = {
[key in UserRole]: key extends 'admin' ? string[] : boolean;
};
const permissions: RolePermissions = {
admin: ['create', 'delete'],
user: true,
// moderator: ... // обязателен
};
Когда использовать индексную сигнатуру, а когда mapped type?
- Индексная сигнатура — если ключи могут быть любыми (например, словарь строк).
- Mapped type — если ключи строго фиксированы (enum, union строк, ключи другого объекта).
В вашем случае mapped type — идеальное решение. Оно даёт строгую типизацию и автодополнение в редакторе.
<<Альтернативный подход: использование Record
Если вам не нужно создавать кастомный тип с дополнительными модификаторами, можно воспользоваться встроенной утилитой Record<Keys, Type>. Она делает то же самое, что и mapped type, но в более краткой форме:
type UserRole = 'admin' | 'user';
type Settings = Record<UserRole, boolean>;
const config: Settings = {
admin: true,
user: false,
};
Record принимает два аргумента: первый — union ключей, второй — тип значений. Это эквивалентно { [key in UserRole]: boolean }. Такой подход удобен для простых случаев, когда не нужно добавлять readonly или опциональность.
Когда Record не подходит
Если вам нужно, чтобы некоторые поля были опциональными или имели разные типы, лучше использовать явный mapped type. Например:
type UserRole = 'admin' | 'user';
// Record не позволяет смешивать типы значений
type Settings = Record<UserRole, boolean>; // Все поля — boolean
// А mapped type даёт гибкость:
type FlexibleSettings = {
[key in UserRole]: key extends 'admin' ? string : boolean;
};
В целом, Record — это синтаксический сахар для самых частых случаев. Для вашей исходной задачи он подходит идеально.
Ответов пока нет.
Войдите чтобы оставить ответ