Avatar ·

Ошибка TypeScript: индексный параметр не может быть объединением (union) типов

Всем привет! Я новичок в TypeScript и столкнулся с непонятной ошибкой. Пытаюсь создать объект, в котором ключи могут быть только двух типов строк: `'user'` или `'admin'`. Написал такой код: ```typescript type Role = 'user' | 'admin'; interface UserData { name: string; age: number; } // Пытаюсь создать объект, где ключ — роль, а значение — данные пользователя const users: { [key: Role]: UserData } = {}; ``` Но TypeScript выдаёт ошибку: *"An index signature parameter type cannot be a union type. Consider using a mapped object type instead"*. Я понимаю, что индексные сигнатуры обычно работают с `string`, `number` или `symbol`, но почему нельзя использовать объединение литеральных строк? Ведь это же тоже строки! Мне нужно, чтобы объект `users` гарантированно содержал только ключи `'user'` и `'admin'`, а при попытке обратиться к `users['guest']` TypeScript выдавал ошибку на этапе компиляции. Как правильно это сделать? Может, есть какой-то другой синтаксис для таких случаев? Я пробовал использовать `Record`, но тоже получаю похожую ошибку. Подскажите, пожалуйста, как решить эту проблему и почему TypeScript так строг к индексным сигнатурам? Заранее спасибо! <<>>

Понимание проблемы: почему 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, и их стоит изучить в первую очередь.