How to fix Type 'X' cannot be used as an index type in TypeScript

TypeScriptINTERMEDIATEMEDIUMUpdated Dec 11, 2025ยท 178 views

The TypeScript compiler emits TS2538 when a type that could include non-PropertyKey values is used for an index signature or mapped type. Narrowing the key type so it extends PropertyKey (string | number | symbol) restores the mapping.

Quick fix

Fixes TS2538

type MapToStrings<K extends PropertyKey> = {
  [Key in K]: string;
};
  1. Constrain generic keys to PropertyKey
  2. Narrow unions before indexing
  3. Cast runtime key arrays to property keys
  4. Rely on Record<PropertyKey, ...> helpers
typescript โ€” error
type MapToStrings<K extends PropertyKey> = {
[Key in K]: string;
};
Type 'X' cannot be used as an index type

What this error means

TypeScript requires every index signature or `[P in ...]` mapped type to use a property key, which is an alias for `string | number | symbol` (the built-in `PropertyKey`). When a type parameter, union, or other alias might include booleans, objects, or undefined, the compiler cannot guarantee it is safe to use as an index. TS2538 surfaces because the key type does not extend `PropertyKey`, so TypeScript refuses to synthesize the mapped structure until you constrain it.

Typical symptoms

  • TS2538 appears while declaring `[P in K]` or `T[K]`, even though the runtime keys are valid strings or numbers.
  • The IDE underlines the generic key type with "Type 'X' cannot be used as an index type" while building a Record-style helper.
  • Helper types that pipe `Object.keys`, `Map.keys()`, or tuple indexes into another mapped type suddenly fail when tightening generics.
  • You get the error after switching to `satisfies`/`as const` or template literal unions but before narrowing the key type explicitly.
  • Code works at runtime but TypeScript refuses to compile because the key type might include non-PropertyKey values.

Common causes

  1. A generic key type parameter is unconstrained, so it can resolve to `object`, `undefined`, or other non-indexable values.
  2. You use a union type for keys that mixes strings with callbacks, arrays, or `never` without narrowing the union via `Extract<keyof any, K>`.
  3. The mapped type forwards the return type of `Object.keys` or tuple indexes without casting them to `PropertyKey`.
  4. You try to reuse a class, interface, or tuple type as the index type even though it is not assignable to `string | number | symbol`.
  5. You expect the compiler to infer that a literal union is safe, but the inference includes `string` or `number` and TypeScript cannot prove the union extends `PropertyKey`.

How to fix "Type 'X' cannot be used as an index type"

1Constrain generic keys to PropertyKey

Limit the type parameter to PropertyKey so TypeScript knows it can act as an object index:

ts
type MapToStrings<K extends PropertyKey> = {
  [Key in K]: string;
};

Without the K extends PropertyKey constraint, the compiler treats K as an arbitrary type and throws TS2538.

2Narrow unions before indexing

If you derive keys from a broader union, intersect them with keyof any or use Extract<keyof any, ...>:

ts
type SafeKeys<RawKeys> = Extract<RawKeys, keyof any>;
type SafeLookup<T, K> = T[SafeKeys<K>];

Since keyof any expands to string | number | symbol, the extract prevents invalid members from slipping into the mapped type and triggering TS2538.

3Cast runtime key arrays to property keys

When working with Object.keys, Object.entries, or DOM APIs, cast the returned strings back to keyof typeof obj before using them as indexes:

ts
const keys = Object.keys(obj) as Array<keyof typeof obj>;
keys.forEach((key) => {
  const value = obj[key];
});

The compiler now sees key as a valid property key, which belongs to keyof typeof obj, so it stops complaining about TS2538.

4Rely on Record<PropertyKey, ...> helpers

If you only need a dictionary with runtime keys, wrap the shape with Record<PropertyKey, Value> or an index signature:

ts
type LooseDictionary<Value> = Record<PropertyKey, Value>;

const bag: LooseDictionary<number> = {};
bag['foo'] = 1;
bag[Symbol('bar')] = 2;

Record ensures TypeScript already knows the keys are legal, which avoids TS2538 when you later reference the values.

Advanced notes

PropertyKey is the union string | number | symbol, and keyof any produces the same alias because every property key in the runtime world must be one of those three primitive types. When you constrain generics to PropertyKey or keyof any, you guarantee the compiler can safely iterate over the keys, even if the type comes from template literal unions, tuple indices, or helper utilities. Because TS2538 only triggers at compile time, you can keep the runtime behavior as-is while satisfying the type checker by adding the constraint or an Extract/as cast.

Frequently asked questions

What does "TS2538" mean?
] mapped type to use a property key, which is an alias for string | number | symbol (the built-in PropertyKey). When a type parameter, union, or other alias might include booleans, objects, or undefined, the compiler cannot guarantee it is safe to use as an index. TS2538 surfaces because the key type does not extend PropertyKey, so TypeScript refuses to synthesize the mapped structure until you constrain it.
How do I fix "TS2538"?
The TypeScript compiler emits TS2538 when a type that could include non-PropertyKey values is used for an index signature or mapped type. Narrowing the key type so it extends PropertyKey (string | number | symbol) restores the mapping.
What causes "TS2538"?
A generic key type parameter is unconstrained, so it can resolve to object, undefined, or other non-indexable values. You use a union type for keys that mixes strings with callbacks, arrays, or never without narrowing the union via Extract<keyof any, K>. The mapped type forwards the return type of Object.keys or tuple indexes without casting them to PropertyKey.
Is the "TS2538" error serious?
It is a medium-severity error โ€” more of a blocker than a risk. The steps above resolve it.

Official resources & further reading