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 Tof an existing type and so keeps its shape and copies its modifiers. - Type variable
- A name like
Kthat 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 inKmapped toV. - 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
readonlyor?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".
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 }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).
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
Predict each modifier
In the second example write
type E = MyPartial<MyReadonly<User>>;and predict whetheridisreadonlyand optional before you hover. Both:readonly id?: number. - 2
Grow the table
In the third example add
"owner"toRole. Predict the errors.cancomplains thatowneris missing (TS2741), andmissingcomplains about bothownerandviewer(TS2739). Fixcanfirst. - 3
Turn off the copy
In the last example replace
type A = Copy<Todo>;withtype A = { [K in "id" | "title" | "done"]: Todo[K] };(a hand-written key list;Copy<T>itself can't take one, becauseT[K]needsKto be a key ofT). Doesa.id = 2still fail? It stops failing, because the modifiers are no longer copied. A new error appears on the line that createsa:doneis now required.
Code & diagrams
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 }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)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' ] }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.
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.
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] }withK extends keyof T. It looks like a hand-written key list, but becauseKis constrained tokeyof Tthe compiler still treats it as homomorphic and keeps the modifiers. A plainK extends stringwouldn't. - ▸
Homomorphic mapped types on primitives return the primitive unchanged:
Partial<string>isstring. They also distribute over unions:Partial<A | B>isPartial<A> | Partial<B>. That is why helpers likeReadonlywork 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 whenTis 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
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 aforloop: "for each keyKin the keys ofT, make a property calledKwhose 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 ornull.Kis a type variable that takes each key in turn. - 2
The list after
incan be any union of text, number or symbol literal types. Withkeyof Tyou 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 aboolean. This is howRecord<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
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; writingreadonlybefore it makes them read-only. A+or-in front says add or remove:-?makes optional properties required and-readonlymakes read-only properties writable.+is the default and can be left out. Those four give youPartial,Required,Readonlyand aMutablethat TypeScript doesn't ship. - 4
A mapped type over
keyof Tis called homomorphic (it keeps the same shape asT), and it is special: it *copies* thereadonlyand?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 withK extends keyof T(thePickpattern). - 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, andWrapped<string[]>is an array. It does not turn the array into an object full of methods. This is why built-in helpers likePartialandReadonlybehave well on arrays. - 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
How does a mapped type work? Read { [K in keyof T]: T[K] | null } aloud.
How do you add or remove readonly and optionality in a mapped type, and what do + and - do?
What is a homomorphic mapped type, and why does it matter?
What does a mapped type do when T is an array or a tuple?
Practice
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.
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
readonlyand?with+and-.I can build
Partial,Required,ReadonlyandMutablefrom 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.