This TypeScript error occurs when using the 'in' operator in a mapped type with a non-union type. Mapped types require union types as their source to iterate over. The fix involves ensuring the type parameter is a union type or using conditional types to handle non-union cases.
Fixes TS2589
// WRONG - T could be any type
type Mapped<T> = {
[K in T]: string; // Error: T might not be a union
};
// CORRECT - Constrain T to be string | number | symbol
type Mapped<T extends string | number | symbol> = {
[K in T]: string;
};
// Even better: Constrain to string literals specifically
type StringMapped<T extends string> = {
[K in T]: string;
};
// Usage examples:
type Keys = "name" | "age" | "email";
type Result = Mapped<Keys>; // OK: Keys is a union
// This will work:
const example: Result = {
name: "John",
age: "30",
email: "[email protected]"
};// WRONG - T could be any typetype Mapped<T> = {[K in T]: string; // Error: T might not be a union};// CORRECT - Constrain T to be string | number | symboltype Mapped<T extends string | number | symbol> = {[K in T]: string;};// Even better: Constrain to string literals specificallytype StringMapped<T extends string> = {[K in T]: string;};// Usage examples:type Keys = "name" | "age" | "email";type Result = Mapped<Keys>; // OK: Keys is a union// This will work:const example: Result = {name: "John",age: "30",email: "[email protected]"};Mapped type 'X' cannot use 'in' with non-union type
The "Mapped type 'X' cannot use 'in' with non-union type" error appears when TypeScript encounters a mapped type that tries to use the 'in' operator with a type that isn't a union. Mapped types in TypeScript are designed to transform each member of a union type into a new type structure. In TypeScript, mapped types use the syntax `{[K in Keys]: Type}` where `Keys` must be a union type (like `"a" | "b" | "c"`). The 'in' operator iterates over each member of this union type. When you provide a non-union type (like a single string literal type `"key"` or a generic type parameter that isn't constrained to be a union), TypeScript cannot determine what to iterate over. This error commonly occurs when: 1. Using a generic type parameter that isn't constrained to be a union 2. Passing a single string literal type instead of a union 3. Using conditional types that narrow to non-union types 4. Working with utility types that expect union inputs
Ensure your generic type parameter is constrained to be a union of string/number literals:
// WRONG - T could be any type
type Mapped<T> = {
[K in T]: string; // Error: T might not be a union
};
// CORRECT - Constrain T to be string | number | symbol
type Mapped<T extends string | number | symbol> = {
[K in T]: string;
};
// Even better: Constrain to string literals specifically
type StringMapped<T extends string> = {
[K in T]: string;
};
// Usage examples:
type Keys = "name" | "age" | "email";
type Result = Mapped<Keys>; // OK: Keys is a union
// This will work:
const example: Result = {
name: "John",
age: "30",
email: "[email protected]"
};The constraint T extends string | number | symbol ensures T can be used with 'in' operator.
If you have a single type that needs to be used in a mapped type, convert it to a union:
// WRONG - Single string literal
type SingleKey = "id";
type Mapped1 = {
[K in SingleKey]: string; // Error: SingleKey is not a union
};
// CORRECT - Make it a union (even if just one member)
type SingleKeyUnion = "id";
type Mapped2 = {
[K in SingleKeyUnion]: string; // OK: technically a union of one
};
// Better: Use a proper union type
type Keys = "id" | "name" | "value";
type Mapped3 = {
[K in Keys]: string;
};
// Using template literal types to create unions
type Prefix<T extends string> = `${T}_prop`;
type PrefixedKeys = Prefix<"user" | "admin">; // "user_prop" | "admin_prop"
type Mapped4 = {
[K in PrefixedKeys]: boolean;
};Remember: A single string literal type like "id" is technically a union of one, but TypeScript's mapped type semantics require explicit union syntax.
Create mapped types that work with both union and non-union inputs:
// Flexible mapped type that handles any input
type SafeMapped<T> = T extends string | number | symbol
? { [K in T]: string }
: never;
// Usage:
type UnionResult = SafeMapped<"a" | "b">; // { a: string; b: string; }
type SingleResult = SafeMapped<"id">; // { id: string; }
type InvalidResult = SafeMapped<object>; // never
// More advanced: Handle arrays and convert to unions
type ArrayToUnion<T extends readonly any[]> = T[number];
type KeysFromArray = ArrayToUnion<["name", "age", "email"]>; // "name" | "age" | "email"
type MappedFromArray<T extends readonly string[]> = {
[K in ArrayToUnion<T>]: string;
};
// Usage:
const keys = ["name", "age"] as const;
type Result = MappedFromArray<typeof keys>; // { name: string; age: string; }Conditional types let you handle edge cases gracefully while maintaining type safety.
The keyof operator can produce unexpected results. Ensure you're using it on object types:
// WRONG - keyof on primitive returns never
type PrimitiveKeys = keyof string; // never
type Mapped1 = {
[K in PrimitiveKeys]: any; // Error: never is not a union
};
// CORRECT - Use keyof on object types
interface User {
id: number;
name: string;
email: string;
}
type UserKeys = keyof User; // "id" | "name" | "email"
type Mapped2 = {
[K in UserKeys]: string;
};
// Handle optional properties
interface PartialUser {
id?: number;
name?: string;
}
type PartialKeys = keyof PartialUser; // "id" | "name"
type RequiredMapped = {
[K in PartialKeys]-?: string; // Remove optionality
};
// Using keyof with generics
type GenericMapped<T extends object> = {
[K in keyof T]: T[K];
};
// This ensures T is an object type
type Result = GenericMapped<{ a: number; b: string }>; // { a: number; b: string; }Always constrain generic parameters to object when using keyof to avoid never results.
When working with unions in mapped types, use distributive conditional types:
// Non-distributive (wrong approach)
type NonDistributive<T> = T extends any ? { [K in T]: string } : never;
// Error: T in mapped type might not be union
// Distributive mapped type (correct)
type DistributiveMapped<T> = T extends any
? { [K in T]: string }
: never;
// Even better: Extract to helper type
type ToUnion<T> = T extends any ? T : never;
type SafeMapped<T> = { [K in ToUnion<T>]: string };
// Usage with complex unions
type Status = "pending" | "success" | "error";
type WithData<T> = T extends any
? { status: T; data: string }
: never;
type Result = WithData<Status>;
// Equivalent to:
// { status: "pending"; data: string } |
// { status: "success"; data: string } |
// { status: "error"; data: string }
// Transforming union members individually
type MakeOptional<T> = T extends any
? { [K in T]?: string }
: never;
type OptionalResult = MakeOptional<"a" | "b">; // { a?: string } | { b?: string }Distributive conditional types ensure each union member is processed separately.
Use TypeScript's built-in utilities to debug type issues:
// Check if a type is a union
type IsUnion<T> = [T] extends [T & any] ? false : true;
// Test it:
type Test1 = IsUnion<"a">; // false
type Test2 = IsUnion<"a" | "b">; // true
// Extract union members
type UnionMembers<T> = T extends any ? T : never;
// Debug what keyof produces
interface Example {
x: number;
y: string;
}
type Keys = keyof Example; // "x" | "y"
type KeyType = Keys extends string ? "string" : "other"; // "string"
// Use type assertions to narrow types
function processKeys<T extends string>(keys: T[]): { [K in T]: string } {
// Type assertion to help TypeScript understand
const keyUnion = keys[0] as T;
return {} as any; // Implementation
}
// Create type guards
function isStringUnion<T>(value: T): value is T & string {
return typeof value === "string";
}
// Use satisfies for type checking
const config = {
keys: ["id", "name"] as const,
values: { id: "123", name: "John" }
} satisfies {
keys: readonly string[];
values: Record<string, string>;
};These utilities help identify why a type isn't working as a union in mapped types.
### Mapped Type Mechanics
TypeScript's mapped types operate at the type level, not runtime. The 'in' operator in {[K in Keys]: Type} requires Keys to be a union type because:
1. Iteration Semantics: Each member of the union becomes a key in the resulting object type
2. Distributivity: Mapped types distribute over union members
3. Type Safety: Non-union types can't be iterated in this context
### Union vs. Intersection in Mapped Types
While unions work with 'in', intersections do not:
// Union - works
type UnionKeys = "a" | "b";
type FromUnion = { [K in UnionKeys]: string }; // { a: string; b: string; }
// Intersection - doesn't work with 'in'
type IntersectionKeys = "a" & "b"; // never
type FromIntersection = { [K in IntersectionKeys]: string }; // {}### Template Literal Types and Mapped Types
Template literal types create unions automatically:
type Prefix = "user" | "admin";
type Suffix = "Id" | "Name";
type Combined = `${Prefix}_${Suffix}`;
// "user_Id" | "user_Name" | "admin_Id" | "admin_Name"
type Mapped = { [K in Combined]: string };
// {
// user_Id: string;
// user_Name: string;
// admin_Id: string;
// admin_Name: string;
// }### Conditional Type Inference
When using conditional types with mapped types, inference can be tricky:
type InferMapped<T> = T extends infer U
? { [K in U & string]: string }
: never;
// U & string ensures we only get string keys
// With constraints
type ConstrainedMapped<T> = T extends string
? { [K in T]: string }
: { error: "Not a string type" };### Utility Types That Create Unions
Several TypeScript utilities produce union types suitable for mapped types:
- keyof T: Union of keys (when T is object)
- T[number]: Union of array element types
- Extract<T, U>: Union of T members assignable to U
- Exclude<T, U>: Union of T members not assignable to U
### Performance Considerations
Complex mapped types with large unions can slow down TypeScript compilation. Consider:
1. Limiting union size: Keep unions under 100 members when possible
2. Using indexed access: T[keyof T] instead of mapped types for simple transformations
3. Memoization: Store intermediate type results
4. Avoiding recursion: Deeply nested mapped types cause exponential type checking
### Compatibility with Other Type Features
Mapped types work with:
- Optional properties: { [K in Keys]?: Type }
- Readonly properties: { readonly [K in Keys]: Type }
- Modifiers: { -readonly [K in Keys]: Type } removes readonly
- Template strings: As shown above
But be careful with:
- Index signatures: Can conflict with mapped types
- Excess property checking: Mapped types don't prevent extra properties
- Branded types: May lose branding information