๐ŸŽ“ A1Learner
๐Ÿ”
0 of 37 lessons completed

TypeScript Mapped Type Patterns

Mapped type patterns are reusable shapes that reshape a whole object type in one declaration. Each pattern states the transformation once and lets the compiler apply it to every key.

A mutable copy from a readonly source

The source interface marks both members readonly. The map rebuilds the same members with the readonly modifier removed.

interface Settings {
  readonly theme: string;
  readonly volume: number;
}
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
const draft: Mutable<Settings> = { theme: 'dark', volume: 5 };
draft.volume = 8;
console.log(draft.theme);
console.log(draft.volume);

An event map from a payload record

The payload record holds one payload shape per event. The map renames each key into a handler name and types the handler from the matching payload.

type Payloads = {
  login: { user: string };
  purchase: { item: string; price: number };
};
type Handlers = {
  [K in keyof Payloads as `on${Capitalize<string & K>}`]: (payload: Payloads[K]) => void;
};
const handlers: Handlers = {
  onLogin: (p) => console.log('login ' + p.user),
  onPurchase: (p) => console.log('purchase ' + p.item),
};
handlers.onLogin({ user: 'Ana' });
handlers.onPurchase({ item: 'Pen', price: 3 });

Optionality flipped for one key

The pattern rebuilds a profile with the bio member optional and every other member required. A value without a bio still satisfies the built type.

interface Profile {
  name: string;
  bio: string;
  website: string;
}
type BioOptional = Omit<Profile, 'bio'> & Partial<Pick<Profile, 'bio'>>;
const form: BioOptional = { name: 'Ana', website: 'ana.dev' };
console.log(form.name);
console.log(form.bio);

Example

interface Panel {
  readonly title: string;
  readonly width: number;
}
type PanelEvents = {
  resize: { width: number };
  close: { title: string };
};
type PanelHandlers = {
  [K in keyof PanelEvents as `on${Capitalize<string & K>}`]: (payload: PanelEvents[K]) => void;
};
const panel: { -readonly [K in keyof Panel]: Panel[K] } = { title: 'Menu', width: 200 };
panel.width = 240;
const panelHandlers: PanelHandlers = {
  onResize: (p) => console.log('resize to ' + p.width),
  onClose: (p) => console.log('close ' + p.title),
};
console.log(panel.title + ' at ' + panel.width);
panelHandlers.onResize({ width: 240 });
panelHandlers.onClose({ title: 'Menu' });

Key points

  • A pattern names the transformation, not the result keys, so one declaration covers any source type.
  • Removing readonly through a map turns a frozen source type into an editable draft type.
  • Remapping keys with an as clause can rename every key while the mapped values follow the old keys.
  • Each handler name in an event map is built from the payload record keys, so a missing handler is a compile error.
  • Optionality can be flipped for one chosen key while the other keys keep their original shape.
  • Values prove each pattern, since an assignment the built type refuses never reaches runtime.

In this example, the mutable pattern frees the panel fields for editing while the event map pattern derives both handler names and payload types from one record. The two patterns compose because each one only restates how keys and values transform.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท TypeScript Playground

Exercise

In the Handlers map, where do the handler names onLogin and onPurchase come from?