Mapped Types
A mapped type builds a new object type by looping over the keys of another type and transforming each property. It's how Partial, Readonly and Record are implemented — and once you understand the syntax, you can write your own.
The basic syntax
A mapped type looks like an index signature with in: for each key K in some union of keys, produce a property:
type Flags<Keys extends string> = { [K in Keys]: boolean; }; type Features = Flags<"darkMode" | "betaSearch" | "newCheckout">; const features: Features = { darkMode: true, betaSearch: false, newCheckout: true, }; console.log(Object.entries(features).filter(([, on]) => on).map(([name]) => name));
[ 'darkMode', 'newCheckout' ]
Read [K in Keys]: boolean as "for every K in Keys, a property named K of type boolean". That's exactly how the built-in Record<K, V> is defined.
Mapping over another type's keys
Combine in with keyof to transform every property of an existing type. T[K] refers to the original property's type:
type Nullable<T> = { [K in keyof T]: T[K] | null; }; type Stringify<T> = { [K in keyof T]: string; }; interface Point { x: number; y: number } const unknownPos: Nullable<Point> = { x: 10, y: null }; const formatted: Stringify<Point> = { x: "10px", y: "4px" }; console.log(unknownPos, formatted);
{ x: 10, y: null } { x: '10px', y: '4px' }Adding and removing modifiers
A mapped type can add readonly or ? to every property — or remove them with a - prefix:
type MyPartial<T> = { [K in keyof T]?: T[K] }; type MyReadonly<T> = { readonly [K in keyof T]: T[K] }; type Mutable<T> = { -readonly [K in keyof T]: T[K] }; type MyRequired<T> = { [K in keyof T]-?: T[K] }; interface Config { readonly host: string; port?: number; } const c: Mutable<Config> = { host: "a" }; c.host = "b"; const full: MyRequired<Config> = { host: "x" };
error TS2741: Property 'port' is missing in type '{ host: string; }' but required in type 'MyRequired<Config>'.MyPartial and MyReadonly are essentially how the built-in versions are written. -readonly and -? remove those modifiers, which is how Required works.
Renaming keys with as
Since TypeScript 4.1, the as clause lets you compute a new key name for each property, often with a template literal type:
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; }; interface Person { name: string; age: number; } const p: Getters<Person> = { getName: () => "Lin", getAge: () => 40, }; console.log(p.getName(), p.getAge());
Lin 40
string & K narrows the key to strings (keys can also be numbers or symbols), and Capitalize upper-cases the first letter.
Filtering properties
If the as clause produces never, that property is dropped. Combined with a conditional type (next lesson), this filters properties by their type:
type OnlyStrings<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K]; }; interface Product { id: number; name: string; sku: string; inStock: boolean; } const searchable: OnlyStrings<Product> = { name: "Lamp", sku: "L-01" }; console.log(Object.keys(searchable));
[ 'name', 'sku' ]
A practical example: form state
Mapped types shine when one shape needs several "parallel" shapes — like a form that tracks a value, an error and a touched flag per field:
interface SignupForm { email: string; password: string; age: number; } type Errors<T> = { [K in keyof T]?: string }; type Touched<T> = { [K in keyof T]: boolean }; const errors: Errors<SignupForm> = { password: "Too short" }; const touched: Touched<SignupForm> = { email: true, password: true, age: false }; for (const field of Object.keys(touched) as (keyof SignupForm)[]) { if (touched[field] && errors[field]) console.log(`${field}: ${errors[field]}`); }
password: Too short
Add a field to SignupForm and both Errors and Touched pick it up automatically — and touched will fail to compile until you include it.