Ошибка TypeScript: индекс из union-типа — решение для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему TypeScript ругается на ваш код?

Вы когда-нибудь писали на TypeScript код, который кажется абсолютно логичным, но компилятор вдруг выдаёт пугающую ошибку? Если вы сталкивались с сообщением «An index signature parameter type cannot be be a union type. Consider using a mapped object type instead» — вы не одиноки. Эта ошибка — один из самых популярных вопросов на Stack Overflow (тысячи просмотров!), и она часто ставит в тупик даже опытных разработчиков.

В этой статье мы не просто покажем, как исправить ошибку. Мы разберём, почему TypeScript так строг, и как использовать mapped object types, чтобы ваш код стал не только рабочим, но и более безопасным. Поехали!

Что такое index signature и зачем он нужен?

Представьте, что вы создаёте объект, у которого ключи — это строки, а значения — числа. Например, словарь с ценами на товары:

// Обычный объект
const prices = {
  apple: 100,
  banana: 50,
  cherry: 200
};

Но что, если вы хотите сказать TypeScript: «Слушай, у этого объекта могут быть любые строковые ключи, и все значения будут числами»? Для этого и нужен index signature — специальный синтаксис, который описывает шаблон для всех свойств объекта:

// Index signature: [key: string]: number
interface PriceMap {
  [key: string]: number;
}

const prices: PriceMap = {
  apple: 100,
  banana: 50
};

Здесь [key: string]: number — это и есть index signature. Он говорит: «Ключи — строки, значения — числа». Всё просто, правда?

А в чём проблема с union-типами?

Теперь представьте, что вы хотите ограничить возможные ключи только двумя вариантами: 'name' или 'age'. Кажется логичным написать так:

// ❌ ОШИБКА! Так нельзя
interface Person {
  [key: 'name' | 'age']: string | number;
}

И тут TypeScript выдаёт ту самую ошибку: «An index signature parameter type cannot be a union type. Consider using a mapped object type instead».

Почему? Потому что index signature предназначен для описания неограниченного набора ключей. Если вы хотите задать конкретный список ключей, это уже не шаблон, а точное описание структуры. TypeScript не позволяет смешивать эти концепции.

Как исправить ошибку? Используем Mapped Object Type

Решение, которое предлагает сам компилятор — использовать mapped object type (сопоставленный объектный тип). Звучит сложно, но на деле это просто способ «перебрать» все ключи из union-типа и создать для каждого своё свойство.

Вот как это выглядит:

// ✅ ПРАВИЛЬНО: используем mapped type
type Person = {
  [K in 'name' | 'age']: K extends 'name' ? string : number;
};

// Или более простой вариант, если типы значений одинаковы:
type PersonSimple = {
  [K in 'name' | 'age']: string | number;
};

Разберём синтаксис [K in 'name' | 'age']:

  • K — это переменная, которая по очереди принимает каждое значение из union-типа ('name', потом 'age').
  • in — ключевое слово, которое запускает «цикл» по типам.
  • 'name' | 'age' — наш набор ключей.

В результате TypeScript создаст объект с двумя конкретными свойствами: name и age. Ошибка исчезла!

Пример с разными типами значений

Часто бывает, что разные ключи должны иметь разные типы значений. С помощью условных типов (conditional types) это решается элегантно:

type Config = {
  [K in 'host' | 'port' | 'debug']: 
    K extends 'host' ? string :
    K extends 'port' ? number :
    boolean;
};

// Теперь:
const config: Config = {
  host: 'localhost',  // ✅ строка
  port: 3000,         // ✅ число
  debug: true         // ✅ булево
};

Этот код читается как: «Для каждого ключа K из списка, если K — это 'host', то тип — строка; если 'port' — число; иначе — булево». Красиво и безопасно!

Альтернативные решения (когда mapped type не подходит)

Иногда mapped object type — не единственный выход. Рассмотрим пару альтернатив:

1. Использовать Record-тип

Record<K, V> — это встроенный утилитарный тип TypeScript, который делает то же самое, что и mapped type, но с более коротким синтаксисом:

type Person = Record<'name' | 'age', string | number>;

// Эквивалентно:
// type Person = { [K in 'name' | 'age']: string | number };

Минус: Record задаёт один и тот же тип для всех ключей. Если нужны разные типы — придётся вернуться к mapped type + conditional.

2. Просто перечислить свойства вручную

Самый очевидный способ — не использовать index signature вообще:

interface Person {
  name: string;
  age: number;
}

Это работает, если количество ключей фиксировано и вы знаете их заранее. Но если ключи динамические или их много — лучше использовать mapped type.

Почему TypeScript вообще так сделал? Глубокое погружение

Возможно, вы думаете: «Ну почему нельзя было разрешить index signature с union? Это же так удобно!»

Причина — в безопасности типов. Представьте, что TypeScript разрешил бы такой код:

// Гипотетически (но так нельзя!)
interface Bad {
  [key: 'a' | 'b']: string;
}

const obj: Bad = { a: 'hello' };
obj.b; // ✅ OK, но что если b нет?

Если бы index signature работал с union, то TypeScript считал бы, что все ключи из union обязательно есть в объекте. Но это не так — вы можете создать объект только с одним ключом. Возникает несоответствие: тип говорит «у тебя есть и a, и b», а реально есть только a. Это приводит к ошибкам в рантайме.

Mapped type решает эту проблему: он создаёт конкретные свойства, и TypeScript может проверить, все ли они присутствуют.

Практический пример: конфигурация приложения

Давайте закрепим на реальной задаче. Допустим, вы пишете конфиг для приложения, где ключи — это названия серверов, а значения — их адреса:

// ❌ Неправильно: ошибка!
type ServerConfigBad = {
  [key: 'api' | 'cdn' | 'db']: string;
};

// ✅                

Блоги

Рекомендаций по книгам