This TypeScript error occurs when you try to create a mapped type where the constraint references a type parameter that isn't available in that scope. The fix involves restructuring your type definitions to ensure constraints only reference accessible type parameters or using conditional types instead.
Fixes TS2589
// WRONG - T is not constrained, can't be used in mapped type
type Transform<T> = {
[K in keyof T]: T[K];
};
// CORRECT - Constrain T to be an object type
type Transform<T extends object> = {
[K in keyof T]: T[K];
};
// WRONG - Trying to use arbitrary type parameter as keys
type MapKeys<T, U> = {
[K in T]: U; // Error: T is not a valid key type
};
// CORRECT - Constrain T to be string | number | symbol
type MapKeys<T extends string | number | symbol, U> = {
[K in T]: U;
};// WRONG - T is not constrained, can't be used in mapped typetype Transform<T> = {[K in keyof T]: T[K];};// CORRECT - Constrain T to be an object typetype Transform<T extends object> = {[K in keyof T]: T[K];};// WRONG - Trying to use arbitrary type parameter as keystype MapKeys<T, U> = {[K in T]: U; // Error: T is not a valid key type};// CORRECT - Constrain T to be string | number | symboltype MapKeys<T extends string | number | symbol, U> = {[K in T]: U;};Mapped type constraints cannot reference type parameters
The "Mapped type constraints cannot reference type parameters" error (TS2589) appears when you attempt to create a mapped type with a constraint that references a type parameter that isn't in scope. In TypeScript, mapped types allow you to transform properties of an existing type, but the constraint (the part after "in") must be a type that's accessible in the current context. This error typically happens when: 1. You reference a generic type parameter that isn't available in the mapped type's scope 2. You try to use a type parameter from an outer generic function inside a mapped type 3. You attempt to create recursive or self-referential mapped types with incorrect constraints TypeScript's mapped types follow specific scoping rules: the key type in a mapped type (the part after "in") must be a union type, string literal type, or another type that's directly available, not a type parameter from an outer scope that isn't properly constrained.
The most common cause is using unconstrained type parameters in mapped types. Add proper constraints:
// WRONG - T is not constrained, can't be used in mapped type
type Transform<T> = {
[K in keyof T]: T[K];
};
// CORRECT - Constrain T to be an object type
type Transform<T extends object> = {
[K in keyof T]: T[K];
};
// WRONG - Trying to use arbitrary type parameter as keys
type MapKeys<T, U> = {
[K in T]: U; // Error: T is not a valid key type
};
// CORRECT - Constrain T to be string | number | symbol
type MapKeys<T extends string | number | symbol, U> = {
[K in T]: U;
};Key constraints for mapped types:
- T extends object for object properties
- T extends string | number | symbol for key types
- T extends PropertyKey (built-in type for valid keys)
When you have nested generics, ensure inner mapped types only reference accessible parameters:
// WRONG - Inner mapped type references outer T
type Outer<T> = {
inner: <U>() => {
[K in T]: U; // Error: T not accessible here
};
};
// CORRECT - Pass T as parameter to inner function
type Outer<T> = {
inner: <U>(t: T) => {
[K in keyof T]: U;
};
};
// WRONG - Complex nested generic with scope issues
type DeepTransform<T> = {
[K in keyof T]: <U>() => {
[P in T[K]]: U; // Error: T[K] not valid constraint
};
};
// CORRECT - Extract to separate type with proper constraints
type ValueTransform<V extends PropertyKey, U> = {
[P in V]: U;
};
type DeepTransform<T extends Record<string, PropertyKey>> = {
[K in keyof T]: <U>() => ValueTransform<T[K], U>;
};Remember: Each generic function/type has its own scope. Type parameters from outer scopes aren't automatically available.
For complex transformations, use conditional types which have more flexible scoping:
// WRONG - Trying to do complex filtering in mapped type constraint
type FilterProps<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
// This often works, but for more complex cases:
// CORRECT - Use conditional type with mapped type
type FilterByType<T, U> = {
[K in keyof T]: T[K] extends U ? T[K] : never;
}[keyof T];
// WRONG - Self-referential mapped type
type RecursiveTransform<T> = {
[K in keyof T]: T[K] extends object
? RecursiveTransform<T[K]>
: T[K];
};
// CORRECT - Use conditional type approach
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;Conditional types (T extends U ? X : Y) are evaluated before mapped types, avoiding constraint issues.
Divide and conquer complex type definitions:
// WRONG - Too complex, likely to hit constraint issues
type ComplexTransform<T, U, V> = {
[K in keyof T]: T[K] extends U
? { [P in keyof V]: T[K] }
: { [P in keyof T[K]]: V };
};
// CORRECT - Break into smaller, focused types
type TransformIfExtends<T, U, V> = T extends U
? MapToKeys<V, T>
: MapObjectKeys<T[K], V>;
type MapToKeys<K extends PropertyKey, V> = {
[P in K]: V;
};
type MapObjectKeys<T extends object, V> = {
[K in keyof T]: V;
};
// Then compose them
type ComplexTransform<T, U, V> = {
[K in keyof T]: TransformIfExtends<T[K], U, V>;
};Benefits of smaller types:
- Easier to debug
- Reusable across codebase
- Less likely to hit TypeScript limitations
- Better error messages
Sometimes you don't need a mapped type at all:
// WRONG - Unnecessary mapped type
type GetValues<T> = {
[K in keyof T]: T[K];
};
// CORRECT - Simpler indexed access
type GetValues<T> = T[keyof T];
// WRONG - Complex mapped type that could be simpler
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
// CORRECT - Alternative approach
type PickByType<T, U> = Pick<T, {
[K in keyof T]: T[K] extends U ? K : never
}[keyof T]>;
// WRONG - Mapped type for simple transformation
type Optionalize<T> = {
[K in keyof T]?: T[K];
};
// CORRECT - Use built-in utility type
type Optionalize<T> = Partial<T>;Before creating complex mapped types, check if:
1. Built-in utility types (Partial, Pick, Record) can help
2. Indexed access types (T[keyof T]) are sufficient
3. Conditional types are simpler
When you encounter this error, systematically debug:
// 1. Start with concrete types to verify logic
type Example = { a: string; b: number };
type ConcreteTest = {
[K in keyof Example]: Example[K];
}; // Works
// 2. Add one generic parameter
type Test1<T extends object> = {
[K in keyof T]: T[K];
}; // Works
// 3. Add complexity gradually
type Test2<T extends object, U> = {
[K in keyof T]: T[K] extends U ? T[K] : never;
}; // Works
// 4. Use type inference to see intermediate results
type Debug<T> = T; // Hover over Debug<YourType> to see inferred type
// 5. Simplify constraints
// Instead of: type Complex<T> = { [K in T]: any }
// Try: type Complex<T extends PropertyKey> = { [K in T]: any }
// 6. Use TypeScript playground to isolate issues
// Go to: https://www.typescriptlang.org/play
// Paste minimal reproducing exampleDebugging tips:
- Use extends constraints liberally at first
- Comment out parts to isolate the error
- Check TypeScript version (newer versions have better error messages)
- Search TypeScript GitHub issues for similar problems
### Understanding Type Parameter Scoping
TypeScript's generic type parameters follow lexical scoping rules similar to function parameters:
// Each function has its own scope
function outer<T>() {
// T is available here
function inner<U>() {
// U is available here, but T is also accessible
return { t: {} as T, u: {} as U };
}
return inner;
}
// Similarly with types:
type Outer<T> = {
// T is available here
inner: <U>() => {
// Both T and U are available here
t: T;
u: U;
};
};However, mapped types have additional restrictions because they're creating new object types at compile time.
### Recursive and Self-Referential Types
For recursive types, ensure proper base cases:
// WRONG - Infinite recursion in constraint
type Infinite<T> = {
[K in keyof Infinite<T>]: T[K]; // Error
};
// CORRECT - Conditional base case
type JsonSafe<T> = T extends string | number | boolean | null
? T
: T extends object
? { [K in keyof T]: JsonSafe<T[K]> }
: never;### Template Literal Types with Mapped Types
Template literal types in mapped types have special considerations:
// Valid - template literal type as constraint
type AddPrefix<T extends string> = {
[K in T as `prefix_${K}`]: boolean;
};
// WRONG - referencing type parameter in template literal constraint
type TransformKeys<T, P extends string> = {
[K in keyof T as `${P}_${string & K}`]: T[K]; // May fail
};
// CORRECT - ensure P is string literal type
type TransformKeys<T, P extends string> = {
[K in keyof T as `${P}_${string & K}`]: T[K];
};### TypeScript Version Considerations
This error has evolved across TypeScript versions:
- TypeScript 4.1+: Better error messages, more flexible mapped types
- TypeScript 4.3+: Improved support for template literal types in mapped types
- TypeScript 4.4+: Better control flow analysis for generics
- TypeScript 4.7+: Enhanced instantiation expressions
If you're stuck, try updating to the latest TypeScript version for better error messages and possibly different behavior.
### Alternative: Type-Level Programming Libraries
For extremely complex type transformations, consider libraries like:
- type-fest: Collection of utility types
- ts-toolbelt: Type-level programming utilities
- utility-types: Complementary set of utility types
These libraries have battle-tested implementations that avoid common pitfalls.