Skip to content
Laviyx Learn

Try

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.

Advanced8 min readUpdated
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#

TypeScript
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);
Output
{ 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:

TypeScript
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);
Output
{ x: 5 } { x: 1, y: 2 }

Removing modifiers#

Prefixing a modifier with - removes it. This is how Required is built:

TypeScript
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);
Output
2 Final

Mapping over a union#

The keys do not have to come from keyof; any union of strings works:

TypeScript
type Breakpoint = "sm" | "md" | "lg";
type Columns = { [B in Breakpoint]: number };

const grid: Columns = { sm: 1, md: 2, lg: 4 };
console.log(grid.lg);
Output
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:

TypeScript
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);
Output
Must be 18 or older

Change Signup and the form state type follows.

Remapping keys with as#

An as clause renames keys, usually with template literal types:

TypeScript
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());
Output
Asha 21

Capitalize 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:

TypeScript
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));
Output
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
TypeScript
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);
Output
{ name: 'Ravi', city: null } { name: 'Asha', city: 'Pune' }

Key takeaway#

Finished this lesson?