Avatar ·

Ошибка TypeScript: "An index signature parameter type cannot be a union type" — как исправить?

Приветствую, коллеги! Я новичок в TypeScript и столкнулся с ошибкой, которую не могу понять. Есть объект, который хранит настройки для разных типов пользователей: 'admin' и 'user'. Я хочу создать тип, который гарантирует, что ключи объекта будут только этими строками, а значения — булевыми. Вот мой код: ```typescript type UserRole = 'admin' | 'user'; interface Settings { [key: UserRole]: boolean; // Ошибка! } ``` TypeScript выдает: "An index signature parameter type cannot be be a union type. Consider using a mapped object type instead". Я думал, что union-типы можно использовать в качестве ключей, но компилятор не согласен. Мне нужно, чтобы объект мог содержать только свойства 'admin' и 'user', и чтобы при попытке обратиться к другому ключу (например, 'guest') TypeScript выдавал ошибку. Как это правильно сделать? Я видел, что в документации упоминаются mapped types, но не совсем понимаю, как их применить в моем случае. Буду благодарен за понятное объяснение с примерами. <<>>

Почему возникает ошибка и как её исправить с помощью 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 — это синтаксический сахар для самых частых случаев. Для вашей исходной задачи он подходит идеально.