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.
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.
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