Introduction
Mapped types let you transform an existing object type by iterating over its keys. Think of them as Array.prototype.map() but at the type level. The syntax { [K in keyof T]: T[K] } creates a new type by applying a transformation to each property of T. This is how many of TypeScript’s built-in utility types work under the hood.
Basic Mapped Type
The simplest mapped type creates a clone of an existing type:
type Clone<T> = { [K in keyof T]: T[K] };
interface User {
name: string;
age: number;
}
type UserClone = Clone<User>;
// { name: string; age: number }
Adding Modifiers
Mapped types can apply three modifiers: readonly, ? (optional), and - (remove):
// Make all properties readonly
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
// Make all properties optional
type MyPartial<T> = { [K in keyof T]?: T[K] };
// Remove readonly (the - modifier)
type MyMutable<T> = { -readonly [K in keyof T]: T[K] };
// Remove optional
type MyRequired<T> = { [K in keyof T]-?: T[K] };
These are exactly how the built-in Readonly<T>, Partial<T>, and Required<T> are implemented.
Filtering with as Key Remapping
TypeScript 4.1 introduced key remapping using the as clause. This allows filtering or renaming keys:
// Filter keys that are strings
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface User {
name: string;
age: number;
email: string;
}
type OnlyStrings = StringKeys<User>;
// { name: string; email: string }
Template Literal Key Changes
Combine mapped types with template literal types to transform key names:
// Add a prefix to all keys
type WithPrefix<T, P extends string> = {
[K in keyof T as `${P}_${K & string}`]: T[K];
};
interface User {
name: string;
age: number;
}
type Prefixed = WithPrefix<User, 'user'>;
// { user_name: string; user_age: number }
To convert to getter/setter conventions:
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }
Pick and Omit Implementations
Here’s how Pick and Omit work internally:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
type MyOmit<T, K extends keyof any> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
interface User {
name: string;
age: number;
email: string;
}
type NameOnly = MyPick<User, 'name'>;
// { name: string }
type NoEmail = MyOmit<User, 'email'>;
// { name: string; age: number }
Practical Patterns
Deep Partial (recursive mapped type)
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
Nullable Properties
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
Rename Keys with a Mapping
type RenameKeys<T, M extends Record<string, string>> = {
[K in keyof T as K extends keyof M ? M[K] : K]: T[K];
};
Conclusion
Mapped types are the backbone of TypeScript’s type transformation capabilities. From basic modifiers (readonly, ?) to advanced key remapping with template literals, they give you fine-grained control over how types evolve. Combined with conditional types, they enable the kind of compile-time metaprogramming that makes TypeScript truly powerful.
