Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~23 min· topic 4 of 5

Topic 8.4

Mapped Types

In one line

A mapped type loops over a list of keys and builds a new object type with one property for each, so you can make "the same type, but every field optional" without typing it all again.

Think of it like this

A photocopier with a stencil. You feed in a form with five boxes and set the machine to "make every box a dotted outline". Out comes a new form with the same five boxes, all dotted. A mapped type is a copier for types: it walks over every property of a type and writes a new property for each, changed in the way you describe.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Mapped type
A type that builds an object type by looping over a list of keys and writing one property for each.
Modifier
A small keyword on a property: readonly (can't be changed) or ? (may be missing).
Homomorphic
A mapped type that loops over keyof T of an existing type and so keeps its shape and copies its modifiers.
Type variable
A name like K that stands for one type at a time, here one key at a time.
Record
A built-in mapped type: Record<K, V> is an object with every key in K mapped to V.
Partial
A built-in mapped type that makes every property optional.
Readonly
A built-in mapped type that makes every property read-only.
+ and -
Signs in front of readonly or ? in a mapped type to add or remove that modifier.
Union of literals
A type like "a" | "b" that lists exact allowed values. It can be the list a mapped type loops over.

Step by step

01The loop

Read { [K in keyof T]: string } as "for each key K of T, a property K of type string". Applying it to Settings gives an object of the same three names, all text.

T[K] brings the old type back in. T[K] | null means "the old type, or null".

mapped.tswhole filets
type Settings = { theme: string; fontSize: number; compact: boolean };

type AsText<T>   = { [K in keyof T]: string };
type Nullable<T> = { [K in keyof T]: T[K] | null };

type A = AsText<Settings>;   // { theme: string; fontSize: string; compact: string }
type B = Nullable<Settings>; // { theme: string | null; fontSize: number | null; compact: boolean | null }
The loopdiagram
Rendering diagram…

02Build Partial and friends yourself

Add ? to make every property optional: that's Partial. Add -? to make everything required: Required. Add readonly for Readonly. Add -readonly to remove it (a Mutable type; TypeScript doesn't ship it).

modifiers.tswhole filets
type MyPartial<T>  = { [K in keyof T]?: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type MyReadonly<T> = { +readonly [K in keyof T]: T[K] };
type Mutable<T>    = { -readonly [K in keyof T]: T[K] };

03A hand-written union builds a new type

{ [R in Role]: boolean } with type Role = "admin" | "editor" | "viewer" has exactly three properties. Leave one out of an object of this type and you get an error that names it. When you add a fourth role, every object typed this way must grow, which is what you want for a lookup table.

The key list can also come from a frozen array: (typeof days)[number] (Topic 8.3) is a union of the real day names.

04Modifiers are copied only by homomorphic mapped types

Copy Todo with { [K in keyof T]: T[K] } and readonly id and done? stay as they were. Loop over the same names written by hand and the copy loses readonly and ?: done becomes required, and its type is boolean | undefined.

05Arrays and tuples are mapped element by element

Given [string, number, boolean], a mapped type returns a tuple of the same length, one transformed element per slot. Given string[] it returns an array. No properties like length or push get mapped.

Try it yourself

  1. 1

    Predict each modifier

    In the second example write type E = MyPartial<MyReadonly<User>>; and predict whether id is readonly and optional before you hover. Both: readonly id?: number.

  2. 2

    Grow the table

    In the third example add "owner" to Role. Predict the errors. can complains that owner is missing (TS2741), and missing complains about both owner and viewer (TS2739). Fix can first.

  3. 3

    Turn off the copy

    In the last example replace type A = Copy<Todo>; with type A = { [K in "id" | "title" | "done"]: Todo[K] }; (a hand-written key list; Copy<T> itself can't take one, because T[K] needs K to be a key of T). Does a.id = 2 still fail? It stops failing, because the modifiers are no longer copied. A new error appears on the line that creates a: done is now required.

Code & diagrams

The loop: change every property's type

Hover the aliases to see the expanded object types the loop produced. The typo line is an error because the mapped type has no dense property; the code still runs.

Console

(nothing was printed)

Expected console output

{ theme: null, fontSize: null, compact: null }
Rebuild Partial, Required, Readonly and Mutable

Compare each result with User. Partial adds ? to id and name. Required removes it from email. Readonly adds readonly to name and email, and Mutable takes it off id.

Console

(nothing was printed)
A union of names builds a brand new type

Add "owner" to Role: can and missing both need a new property at once. missing is an error today because viewer isn't there. The Schedule keys come from the frozen days list.

Console

(nothing was printed)

Expected console output

{ admin: true, editor: true, viewer: false }
{ mon: [ 'gym' ] }
Copying modifiers, arrays and tuples

Copy<Todo> keeps readonly id and the optional done, so a.id = 2 is rejected and a needs no done. Fixed uses a hand-written key list: no modifiers are copied, so done is required and b is an error. Wrapped on a tuple returns a tuple.

Console

(nothing was printed)

Expected console output

[ { value: 'x' }, { value: 1 }, { value: true } ]

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget one entry in a table

In the third example, the missing object leaves out viewer. Add viewer: false to fix it, then add a fourth role to the union and see the same error return.

terminal
$ Types panel
── what you'll see ──
index.ts(5,7): error TS2741: Property 'viewer' is missing in type '{ admin: true; editor: true; }' but required in type 'Permissions'.

Break #2

Change a read-only field

In the last example, look at a.id = 2;. Remove readonly from id in Todo and the error disappears.

terminal
$ Types panel
── what you'll see ──
index.ts(13,3): error TS2540: Cannot assign to 'id' because it is a read-only property.

Myth vs fact

Myth

A mapped type is a runtime loop that copies an object.

Fact

It runs only in the compiler and is erased. To copy values at runtime you still write JavaScript, such as { ...obj }.

Myth

{ [K in keyof T]: T[K] } is a no-op, so it's pointless.

Fact

It makes a fresh, fully expanded object type, which is useful to see a type clearly. And it is the starting point for every transformation.

Myth

Mapped types always keep readonly and ?.

Fact

Only homomorphic ones (looping over keyof T, or a key type constrained by keyof T). A hand-written key list builds required, writable properties.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The built-in Pick<T, K> is { [P in K]: T[P] } with K extends keyof T. It looks like a hand-written key list, but because K is constrained to keyof T the compiler still treats it as homomorphic and keeps the modifiers. A plain K extends string wouldn't.

  • ▸

    Homomorphic mapped types on primitives return the primitive unchanged: Partial<string> is string. They also distribute over unions: Partial<A | B> is Partial<A> | Partial<B>. That is why helpers like Readonly work with discriminated unions.

  • ▸

    On arrays and tuples the modifiers act on the elements: with a hand-written { [K in keyof T]?: T[K] }, Partial<[string, number]> is [string?, number?] (optional positions), Partial<string[]> is (string | undefined)[], and labels and optional positions of a tuple survive a mapped type ([a: string, b?: number] stays labelled). This only happens when T is a type parameter that gets instantiated with the array: a mapped type written directly over a concrete array type maps its properties like any object.

  • ▸

    A mapped type is lazy until used. A giant chain of mapped types costs compile time only where it's instantiated, but each instantiation creates new type objects. TypeScript caches results per type-argument list, so reuse of the same instantiation is cheap, and thousands of different ones are not.

Remember this

  1. 1

    The problem this solves: you need a second type that is the first one with every field changed in the same way (all optional, all read-only, all null-able), and typing it by hand means two copies that drift apart. The syntax is { [K in keyof T]: ... }. Read it like a for loop: "for each key K in the keys of T, make a property called K whose type is ...". Inside, T[K] (Topic 8.3) is the original property type, so { [K in keyof T]: T[K] | null } gives every field the old type or null. K is a type variable that takes each key in turn.

  2. 2

    The list after in can be any union of text, number or symbol literal types. With keyof T you copy another type. With a hand-written union it builds a brand new object: { [R in "admin" | "editor" | "viewer"]: boolean } has exactly those three properties, each a boolean. This is how Record<K, V> (Topic 3.6) is made, and it is a safe replacement for a table: if you add a role to the union, every object typed with the mapped type must add it too.

  3. 3

    You can change two modifiers on each property: readonly (can't be changed after creation) and ? (the property may be missing). Writing ? after the bracket makes properties optional; writing readonly before it makes them read-only. A + or - in front says add or remove: -? makes optional properties required and -readonly makes read-only properties writable. + is the default and can be left out. Those four give you Partial, Required, Readonly and a Mutable that TypeScript doesn't ship.

  4. 4

    A mapped type over keyof T is called homomorphic (it keeps the same shape as T), and it is special: it *copies* the readonly and ? modifiers of each original property. Copy a type with { [K in keyof T]: T[K] } and you get the same optional and read-only fields. If the key list is a plain hand-written union instead, nothing is copied, and the properties come out required and writable. The same holds when the key type is a type parameter constrained with K extends keyof T (the Pick pattern).

  5. 5

    When a homomorphic mapped type is given an array or a tuple instead of an object, it maps the elements, not the property names: Wrapped<[string, number]> is a tuple of two wrapped values, and Wrapped<string[]> is an array. It does not turn the array into an object full of methods. This is why built-in helpers like Partial and Readonly behave well on arrays.

  6. 6

    Mapped types are erased at runtime like all types. They make no objects and loop over nothing when the program runs. The "loop" is the compiler working out a type, and it runs once per use of the type, not once per value.

Explain it without notes

01

How does a mapped type work? Read { [K in keyof T]: T[K] | null } aloud.

02

How do you add or remove readonly and optionality in a mapped type, and what do + and - do?

03

What is a homomorphic mapped type, and why does it matter?

04

What does a mapped type do when T is an array or a tuple?

Practice

01

Write Nullable<T> that makes each property T[K] | null, and use it on { name: string; age: number } to declare an object with age: null.

02

Write Flags<T> that turns every property of T into a boolean, and use it for type Fields = { id: number; title: string } to get { id: boolean; title: boolean }.

Trade-offs

  • ↔

    Mapped types remove repetition, but error messages show the alias name (Nullable<Settings>) rather than the expanded fields. Hover or add a // ^? to see what a type really is.

  • ↔

    Heavily nested mapped types slow the compiler and confuse teammates. If you need three levels of mapping to explain a type, a plain, named object type may be the better choice (Topic 10.6).

Done when you can

  • I can read { [K in keyof T]: ... } as a loop over keys.

  • I can add and remove readonly and ? with + and -.

  • I can build Partial, Required, Readonly and Mutable from scratch.

  • I can build a table type from a union of names.

  • I can say when a mapped type copies modifiers and when it doesn't.