DEV Community

Cover image for Fix An index signature parameter type cannot be a union
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Fix An index signature parameter type cannot be a union

TL;DR

If you're seeing An index signature parameter type cannot be a union type. Consider using a mapped object type instead, the cause is that TypeScript index signatures only accept string, number, or symbol as the key type. Fix it by replacing the index signature with a mapped object type using the in operator, or the Record utility type.

If that doesn't work, scroll to verify the fix — there are two common variants this guide also covers.

  • Symptom: TypeScript compiler error "An index signature parameter type cannot be a union type. Consider using a mapped object type instead."
  • Root cause: Index signatures describe all possible string/number keys; a union type restricts keys to a specific set, which is a contradiction.
  • Fix: Replace [key: UnionType]: ValueType with [key in UnionType]: ValueType inside a type alias, or use Record<UnionType, ValueType>.
  • Verification: Run tsc --noEmit and confirm zero errors related to the index signature.

What you'll see

A developer on Stack Overflow reported this exact error when trying to use a TypeScript enum as the key type in an interface index signature. The compiler output is unambiguous:

An index signature parameter type cannot be a union type. Consider using a mapped object type instead.
Enter fullscreen mode Exit fullscreen mode

It happens when you write something like this:

enum Option {
  ONE = 'one',
  TWO = 'two',
  THREE = 'three'
}

interface OptionRequirement {
  someBool: boolean;
  someString: string;
}

interface OptionRequirements {
  [key: Option]: OptionRequirement;
  //  ~~~~~~~~~
  //  An index signature parameter type cannot be a union type.
  //  Consider using a mapped object type instead.
}
Enter fullscreen mode Exit fullscreen mode

The behavior is the same across all TypeScript versions that support mapped types. It fires whether you use an enum, a union of string literals, or any type that isn't exactly string, number, or symbol.

Root cause

An index signature in TypeScript describes an object that can have any number of properties, as long as every property's key matches the index signature parameter type and every property's value matches the value type. The language specification restricts the key type to string, number, or symbol — and for good reason.

When you write [key: string]: SomeType, you're telling the compiler "this object may have any string key, and every value under any string key must be SomeType." That's an open-ended contract. A union type like Option.ONE | Option.TWO | Option.THREE is a closed set — it says "only these three specific strings are allowed as keys." Those two ideas are fundamentally incompatible. An index signature cannot both be open-ended (any string) and closed (only these three strings) at the same time.

The compiler detects this contradiction and suggests the correct alternative: a mapped object type. A mapped type iterates over a specific set of keys and produces a type with exactly those keys, which is what you actually want when you use an enum or a union of string literals as the key set.

The relevant code path that triggers the error is any interface or type declaration containing an index signature whose parameter type is not string, number, or symbol:

// Both of these trigger the error:
interface Broken {
  [key: 'a' | 'b']: string; // union type
}

type AlsoBroken = {
  [key: MyEnum]: number; // enum (which is a union under the hood)
};
Enter fullscreen mode Exit fullscreen mode

The fix: replace the index signature with a mapped type

The solution is to use the in operator inside a type alias. The in operator iterates over each member of a union type and creates a property for each one:

enum Option {
  ONE = 'one',
  TWO = 'two',
  THREE = 'three',
}

interface OptionRequirement {
  someBool: boolean;
  someString: string;
}

type OptionRequirements = {
  [key in Option]: OptionRequirement;
};
Enter fullscreen mode Exit fullscreen mode

That single change addresses the cause because key in Option tells TypeScript "for each individual member of the Option union, create a property with that exact key and type OptionRequirement." The result is an object type with exactly three known keys — one, two, and three — each mapped to OptionRequirement. This is a closed, specific shape, not an open-ended index signature.

Step by step

  1. Open the file containing the broken interface or type.
  2. Locate the index signature that uses a union type or enum as the key — it will have the form [key: SomeUnion]: SomeType.
  3. Change the interface to a type alias (mapped types cannot appear inside interface declarations).
  4. Replace the colon (:) after key with the in operator.
  5. Save and run tsc --noEmit to verify.

The fix: use the Record utility type as a shorthand

TypeScript ships with a built-in utility type called Record<K, V> that does exactly what the mapped type above does, but with less syntax:

enum Option {
  ONE = 'one',
  TWO = 'two',
  THREE = 'three',
}

interface OptionRequirement {
  someBool: boolean;
  someString: string;
}

type OptionRequirements = Record<Option, OptionRequirement>;
Enter fullscreen mode Exit fullscreen mode

Record<Option, OptionRequirement> expands to the same mapped type { [key in Option]: OptionRequirement }. It's a direct, readable shortcut. Use it when you don't need to add additional properties or modifiers to the mapped type — Record gives you a plain object type with all keys required and all values of the same type.

If you need to make some keys optional or add readonly modifiers, fall back to the explicit mapped type syntax:

type OptionRequirements = {
  [key in Option]?: OptionRequirement; // all keys optional
};

type ReadonlyOptionRequirements = {
  readonly [key in Option]: OptionRequirement; // all keys readonly
};
Enter fullscreen mode Exit fullscreen mode

Important: mapped types must be inside a type alias, not an interface

A common mistake after learning the fix is to try to use the in operator inside an interface:

// This does NOT compile:
interface OptionRequirements {
  [key in Option]: OptionRequirement;
  //     ~~
  //     'in' operator is not allowed in an interface.
}
Enter fullscreen mode Exit fullscreen mode

TypeScript interfaces do not support mapped type syntax. The in operator, along with keyof, as (key remapping), and other mapped type features, are exclusive to type aliases. If your type needs to be extended by other interfaces or used with implements on a class, you can still use a type alias — classes can implement object types defined with type as long as the shape is a plain object type.

This distinction between interface and type comes up frequently in production TypeScript codebases. I cover the full decision framework in the Interfaces vs Types in TypeScript guide.

Verify the fix

Run the TypeScript compiler in check-only mode to confirm the error is gone:

npx tsc --noEmit
Enter fullscreen mode Exit fullscreen mode

You should see no output (zero errors). If the error persists, check for these two common variants.

Variant A — you used : instead of in

The most frequent mistake after learning about mapped types is writing [key: Options] instead of [key in Options]. The colon tells TypeScript you're still trying to write an index signature, and the error fires again. The fix is to replace the colon with in:

// Wrong — still an index signature:
type ProviderProps = {
  items: {
    [key: PossibleKeysType]: Array<SectionItemsType>;
  };
};

// Correct — mapped type:
type ProviderProps = {
  items: {
    [key in PossibleKeysType]: Array<SectionItemsType>;
  };
};
Enter fullscreen mode Exit fullscreen mode

Variant B — you defined the mapped type inside an interface

If you see an error about the in operator not being allowed, you've placed the mapped type inside an interface body. Convert the interface to a type alias:

// Wrong — interface doesn't support mapped types:
interface OptionRequirements {
  [key in Option]: OptionRequirement;
}

// Correct — type alias does:
type OptionRequirements = {
  [key in Option]: OptionRequirement;
};
Enter fullscreen mode Exit fullscreen mode

Why this happens (and how to avoid it next time)

The invariant is simple: index signatures describe all possible keys of a given category (string, number, symbol). A union type describes a specific, finite set of keys. When you need an object type with a specific set of keys derived from a union or enum, you want a mapped type — not an index signature. The compiler's error message already tells you this, but the distinction between the two concepts is what trips people up.

To prevent this error from appearing in your codebase, enable the @typescript-eslint/consistent-indexed-object-style rule if you're using ESLint with TypeScript. It can enforce using Record or mapped type syntax consistently and will flag index signatures that could be expressed more precisely. For a broader approach to catching type-level mistakes before they reach production, the Type Safety Guide for Next.js + Supabase in TypeScript walks through setting up strict TypeScript configurations that catch these issues at compile time.

FAQ

Why can't I use a union type as an index signature parameter in TypeScript?

TypeScript index signatures require the key type to be string, number, or symbol because they describe all possible keys of an object. A union type restricts the keys to a specific set, which contradicts the open-ended nature of an index signature. Use a mapped object type with the in operator instead — it iterates over the union members and creates a type with exactly those keys.

How do I use an enum as keys in a TypeScript object type?

You cannot use an enum directly in an index signature. Instead, define a mapped type using [key in MyEnum]: ValueType inside a type alias, or use the Record<MyEnum, ValueType> utility type. Both approaches iterate over the enum members and create a type with those specific keys. Mapped types must be defined in a type alias, not an interface.

What is the difference between an index signature and a mapped type?

An index signature ([key: string]: T) says "this object can have any string key, and every value must be T." It's open-ended. A mapped type ([key in SomeUnion]: T) says "this object has exactly the keys in SomeUnion, and each value is T." It's closed and specific. Use index signatures when the set of keys is truly unbounded; use mapped types when you know the exact keys ahead of time.

Can I use a union of string literals as keys in an interface?

Not with an index signature. If you need an interface with a specific set of keys from a union, you have two options: either write the keys out explicitly in the interface, or use a type alias with a mapped type. Interfaces do not support the in operator or mapped type syntax. If your type doesn't need to be merged via declaration merging, a type alias is the more flexible choice.

Related


Originally published at https://www.iloveblogs.blog

Top comments (0)