Mapped Types
Contents
- Basic Syntax
- Simple Examples
- Preserving Original Keys
- Key Remapping with
as - Filtering Keys
- Transforming Property Types
- Deep Mapped Types
- Practical Examples
- Combining Mapped Types
- Index Signatures in Mapped Types
- Common Pitfalls
- When to Use Mapped Types
Overview
Mapped types allow you to create new types by transforming each property of an existing type. They iterate over keys and apply transformations to create new type structures.
Basic Syntax
type MappedType<T> = {
[K in keyof T]: TransformedType;
};
Simple Examples
Make All Properties Optional
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
interface User {
id: string;
name: string;
email: string;
}
type PartialUser = MyPartial<User>;
// { id?: string; name?: string; email?: string }
Make All Properties Required
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // -? removes optional modifier
};
Make All Properties Readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
Make All Properties Mutable
type Mutable<T> = {
-readonly [K in keyof T]: T[K]; // -readonly removes readonly modifier
};
Preserving Original Keys
When you just iterate over keyof T, you preserve the original keys:
type Preserve<T> = {
[K in keyof T]: T[K]; // Same type, just recreated
};
Key Remapping with as
Transform keys while mapping using the as clause:
type RemapKeys<T> = {
[K in keyof T as NewKeyType]: T[K];
};
Add Prefix to Keys
type Prefixed<T, P extends string> = {
[K in keyof T as K extends string ? `${P}${K}` : K]: T[K];
};
interface User {
name: string;
age: number;
}
type PrefixedUser = Prefixed<User, "user_">;
// { user_name: string; user_age: number }
Remove Keys by Remapping to never
type RemoveFields<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type UserWithoutEmail = RemoveFields<User, "email">;
// { id: string; name: string }
Transform Keys
type RemoveMapsPrefixFromObj<T> = {
[K in keyof T as RemoveMaps<K>]: T[K];
};
type RemoveMaps<T> = T extends `maps:${infer Rest}` ? Rest : T;
interface ApiData {
"maps:longitude": string;
"maps:latitude": string;
}
type CleanData = RemoveMapsPrefixFromObj<ApiData>;
// { longitude: string; latitude: string }
Filtering Keys
Use conditional types in the as clause to filter:
// Only keep string properties
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }
Keep Only Required Properties
type RequiredKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];
type OnlyRequired<T> = Pick<T, RequiredKeys<T>>;
Keep Only Optional Properties
type OptionalKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? K : never;
}[keyof T];
type OnlyOptional<T> = Pick<T, OptionalKeys<T>>;
Transforming Property Types
Wrap All Properties in Promise
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};
interface SyncApi {
getUser(): User;
getPost(): Post;
}
type AsyncApi = Promisify<SyncApi>;
// { getUser: Promise<() => User>; getPost: Promise<() => Post> }
Make All Properties Arrays
type Arrayify<T> = {
[K in keyof T]: T[K][];
};
interface Single {
name: string;
count: number;
}
type Multiple = Arrayify<Single>;
// { name: string[]; count: number[] }
Nullable Properties
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
Deep Mapped Types
Apply transformations recursively:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Nested {
user: {
profile: {
name: string;
};
};
}
type ReadonlyNested = DeepReadonly<Nested>;
// All levels are readonly
Deep Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
Practical Examples
Getters and Setters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void }
Event Handlers
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (
newValue: T[K],
oldValue: T[K]
) => void;
};
interface State {
count: number;
name: string;
}
type StateHandlers = EventHandlers<State>;
// {
// onCountChange: (newValue: number, oldValue: number) => void;
// onNameChange: (newValue: string, oldValue: string) => void;
// }
Form Validation Errors
type ValidationErrors<T> = {
[K in keyof T]?: string[];
};
interface RegistrationForm {
email: string;
password: string;
confirmPassword: string;
}
type RegistrationErrors = ValidationErrors<RegistrationForm>;
// { email?: string[]; password?: string[]; confirmPassword?: string[] }
Combining Mapped Types
Pick with Transformation
type PickAndTransform<T, K extends keyof T> = {
[P in K]: T[P] extends (...args: any[]) => any ? T[P] : Readonly<T[P]>;
};
Merge Two Types
type Merge<A, B> = {
[K in keyof A | keyof B]: K extends keyof B
? B[K]
: K extends keyof A
? A[K]
: never;
};
Index Signatures in Mapped Types
// Create index signature from union
type FromUnion<K extends string, V> = {
[P in K]: V;
};
type Dict = FromUnion<"a" | "b" | "c", number>;
// { a: number; b: number; c: number }
Common Pitfalls
Forgetting String Key Check
Template literals require string keys:
// Error: Type 'K' is not assignable to type 'string'
type Wrong<T> = {
[K in keyof T as `prefix_${K}`]: T[K];
};
// Correct: Check K extends string
type Correct<T> = {
[K in keyof T as K extends string ? `prefix_${K}` : never]: T[K];
};
Losing Modifiers
Remapping can lose optional/readonly modifiers:
// Original optional modifier lost
type Transform<T> = {
[K in keyof T as `new_${string & K}`]: T[K];
};
// Preserve optional with conditional
type TransformPreserve<T> = {
[K in keyof T as `new_${string & K}`]+?: T[K];
};
Infinite Recursion
Deep mapped types can cause issues:
// Potential infinite recursion with circular types
type DeepReadonly<T> = {
readonly [K in keyof T]: DeepReadonly<T[K]>;
};
// Add base case for primitives
type DeepReadonlySafe<T> = T extends object
? { readonly [K in keyof T]: DeepReadonlySafe<T[K]> }
: T;
When to Use Mapped Types
- Type transformations: Change modifiers (optional, readonly)
- Key renaming: Add prefixes, suffixes, or transform key names
- Property filtering: Remove or keep certain properties
- Bulk operations: Apply same transformation to all properties
- Type utilities: Build reusable type transformers