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

TypeScript Mapped Types in Depth

Modifier mapping changes the readonly and optional marks while a mapped type copies an object type. The minus forms, -readonly and -?, remove marks the source type carries.

Removing readonly with -readonly

The source type freezes both members. The mapped type removes the mark, so the copy accepts writes.

type FrozenItem = { readonly id: number; readonly label: string };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
const item: Mutable<FrozenItem> = { id: 1, label: 'Pen' };
item.label = 'Pencil';
console.log(item.id);
console.log(item.label);

Removing optionality with -?

The patch type marks both members optional. The mapped type removes the question marks, so a complete value must supply both members.

type ItemPatch = { label?: string; price?: number };
type Complete<T> = { [K in keyof T]-?: T[K] };
const fullItem: Complete<ItemPatch> = { label: 'Pen', price: 3 };
console.log(fullItem.label);
console.log(fullItem.price);

Add and remove in one map

One map removes readonly and optionality together and leaves the member types untouched. The result is a plain, fully required, writable type.

type DraftSpec = { readonly code: string; note?: string };
type FinalSpec<T> = { -readonly [K in keyof T]-?: T[K] };
const spec: FinalSpec<DraftSpec> = { code: 'A1', note: 'fragile' };
spec.note = 'sturdy';
console.log(spec.code);
console.log(spec.note);

A homomorphic copy keeps readonly

This map adds no minus forms, so it keeps the readonly mark from the source. The write to the copied member is refused.

type FrozenLabel = { readonly label: string };
type ShallowCopy<T> = { [K in keyof T]: T[K] };
const copy: ShallowCopy<FrozenLabel> = { label: 'Pen' };
copy.label = 'Pencil';

Example

type RawSettings = { readonly theme?: string; readonly volume?: number; readonly muted?: boolean };
type Ready<T> = { -readonly [K in keyof T]-?: T[K] };
const settings: Ready<RawSettings> = { theme: 'dark', volume: 7, muted: false };
settings.volume = 9;
console.log(settings.theme);
console.log(settings.volume);
console.log(settings.muted);

Key points

  • Writing -readonly in a mapped type removes the readonly mark from every copied member.
  • Writing -? removes the optional mark, so every member becomes required.
  • The two removal forms combine in a single mapped type.
  • A homomorphic mapped type maps over keyof T and keeps the modifiers of the source by default.

In this example, one mapped type strips both marks from the source at once, turning a frozen all-optional record into a required writable one. The assignment to volume proves the readonly mark is gone.

Note

The plus forms +readonly and +? also exist, and the bare readonly and ? spellings mean the same add.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท TypeScript Playground

Exercise

What does the -? modifier do in a mapped type?