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.
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.
Limit the type parameter to PropertyKey so TypeScript knows it can act as an object index:
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.
If you derive keys from a broader union, intersect them with keyof any or use Extract<keyof any, ...>:
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.
When working with Object.keys, Object.entries, or DOM APIs, cast the returned strings back to keyof typeof obj before using them as indexes:
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.
If you only need a dictionary with runtime keys, wrap the shape with Record<PropertyKey, Value> or an index signature:
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.
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.
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript