Mapped types

TypeScript · Advanced · 14 min

Mapped types let one declaration produce whole families of related types, and they're how Partial, Readonly and Record work.

A mapped type creates an object type by looping over a union of keys:

type Flags<T> = { [K in keyof T]: boolean };

For each key K in keyof T, the result has a property K of type boolean. The right-hand side can use K and T[K], so you can transform each property's type: { [K in keyof T]: () => T[K] } turns every property into a getter function.

Modifiers can be added or removed with + / -:

  • { readonly [K in keyof T]: T[K] } — this is Readonly<T>.
  • { [K in keyof T]?: T[K] } — Partial<T>.
  • { -readonly [K in keyof T]-?: T[K] } — remove both, giving a mutable Required.

When you map over keyof T for a type parameter T, the mapped type is homomorphic: it preserves the original optional and readonly modifiers, and it maps arrays and tuples to arrays and tuples instead of to objects with numeric keys.

Key remapping with as changes or filters the keys:

type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
type OnlyStrings<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] };

Mapping a key to never drops it — that's how you filter properties by their value type. (string & K is needed because keys may also be numbers or symbols, which Capitalize can't take.)

You can also map over any union, not just keyof: { [L in "en" | "fr"]: string } is essentially Record<"en" | "fr", string>.

Mapped types read best when named and kept small; compose several simple ones rather than writing one giant expression.

Example

interface Settings {
  readonly theme: "light" | "dark";
  fontSize?: number;
  language: string;
}

type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Validators<T> = { [K in keyof T]-?: (value: T[K]) => boolean };
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
type StringProps<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] };

const validators: Validators<Settings> = {
  theme: v => v === "light" || v === "dark",
  fontSize: v => v === undefined || v > 6,
  language: v => v.length === 2,
};
const s: Mutable<Settings> = { theme: "dark", language: "en" };
s.theme = "light";                                // allowed: readonly removed
const getters: Getters<Settings> = {
  getTheme: () => s.theme, getFontSize: () => s.fontSize, getLanguage: () => s.language,
};
type Strs = StringProps<Settings>;               // { readonly theme: ...; language: string }
const strs: Strs = { theme: "dark", language: "de" };
console.log(validators.language("en"), getters.getTheme(), strs.language);

Remember

  • { [K in keyof T]: ... } transforms every property of T
  • +/- add or remove readonly and ?
  • as remaps keys; mapping a key to never removes it
  • Homomorphic maps keep modifiers and array/tuple shapes

Watch out for

  • Forgetting string & K when building template-literal keys, since keys can also be number or symbol.

Check yourself

  1. What does type M<T> = { [K in keyof T]-?: T[K] } do to { a?: number }?
  2. How do you drop properties from a mapped type?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/mapped-types.html