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