TypeScript course menu
TypeScript Mapped Types: Transforming Every Property of a Type
Learn TypeScript mapped types: the { [K in keyof T]: ... } syntax, how Partial and Readonly are built, adding and removing readonly and optional modifiers, remapping keys with as, filtering properties, and practical examples such as form state and getters.
On this page
A mapped type creates a new object type by going through each property of an existing type (or each member of a union) and transforming it. It is the type-level equivalent of array.map, and it is how utility types such as Partial and Readonly are written.
The syntax#
type Flags<T> = {
[K in keyof T]: boolean;
};
interface Settings {
darkMode: string;
notifications: number;
autoSave: string;
}
const enabled: Flags<Settings> = { darkMode: true, notifications: false, autoSave: true };
console.log(enabled);{ darkMode: true, notifications: false, autoSave: true }[K in keyof T] loops over every key K of T. The type after the colon is the new type of that property; T[K] refers to the original property's type.
How Partial and Readonly work#
The built-in utility types are short mapped types:
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
interface Point { x: number; y: number }
const p: MyPartial<Point> = { x: 5 };
const q: MyReadonly<Point> = { x: 1, y: 2 };
console.log(p, q);{ x: 5 } { x: 1, y: 2 }Removing modifiers#
Prefixing a modifier with - removes it. This is how Required is built:
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
interface Draft {
readonly id: number;
title?: string;
}
const editable: Mutable<Draft> = { id: 1 };
editable.id = 2; // allowed: readonly removed
const full: MyRequired<Draft> = { id: 3, title: "Final" }; // title now required
console.log(editable.id, full.title);2 FinalMapping over a union#
The keys do not have to come from keyof; any union of strings works:
type Breakpoint = "sm" | "md" | "lg";
type Columns = { [B in Breakpoint]: number };
const grid: Columns = { sm: 1, md: 2, lg: 4 };
console.log(grid.lg);4(For this simple case, Record<Breakpoint, number> does the same; Record is itself a mapped type.)
Transforming property types#
The new type can depend on the old one. Here, every property becomes a field object for a form:
type FormFields<T> = {
[K in keyof T]: { value: T[K]; error: string | null; touched: boolean };
};
interface Signup {
email: string;
age: number;
}
const form: FormFields<Signup> = {
email: { value: "asha@example.com", error: null, touched: true },
age: { value: 17, error: "Must be 18 or older", touched: true },
};
console.log(form.age.error);Must be 18 or olderChange Signup and the form state type follows.
Remapping keys with as#
An as clause renames keys, usually with template literal types:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
const person: Getters<Person> = {
getName: () => "Asha",
getAge: () => 21,
};
console.log(person.getName(), person.getAge());Asha 21Capitalize is a built-in type that upper-cases the first letter of a string literal type. string & K keeps only string keys (keys can also be numbers or symbols).
Filtering properties#
Mapping a key to never with as removes it. This keeps only the properties whose values are functions:
type MethodsOnly<T> = {
[K in keyof T as T[K] extends (...args: never[]) => unknown ? K : never]: T[K];
};
class Cart {
items: string[] = [];
add(item: string): void {
this.items.push(item);
}
count(): number {
return this.items.length;
}
}
const api: MethodsOnly<Cart> = {
add: (item) => console.log("adding", item),
count: () => 0,
};
api.add("Pen");
console.log(Object.keys(api));adding Pen
[ 'add', 'count' ]The ? : in the key position is a conditional type, the subject of the next lesson.
Practice#
Practice exercise
Write a mapped type Nullable<T> that allows null for every property, and a mapped type Setters<T> that produces setName(value: string): void style methods for every property. Use both with an interface Profile { name: string; city: string }.
Show solution
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface Profile {
name: string;
city: string;
}
const partialProfile: Nullable<Profile> = { name: "Ravi", city: null };
const state: Profile = { name: "", city: "" };
const setters: Setters<Profile> = {
setName: (value) => { state.name = value; },
setCity: (value) => { state.city = value; },
};
setters.setName("Asha");
setters.setCity("Pune");
console.log(partialProfile, state);{ name: 'Ravi', city: null } { name: 'Asha', city: 'Pune' }Key takeaway#
Finished this lesson?