Mapped types
Build a new type from every property of another: make them optional or read-only, give them a new type, rename keys with as, and leave keys out.
- 01Read and write a mapped typeExplain how
{ [Key in keyof T]: T[Key] }goes through every key of a type and builds a property for each. - 02Change modifiersAdd
?orreadonlyto every property, and take them away with-?and-readonly. - 03Rename and filter keysUse
asto give keys new names, and to leave a key out by turning it intonever.
A flag for every field of a form
A signup form has a name, an email, and an age. To show an error only after someone has left a field, the page keeps a "touched" flag for each field: true or false. You could write that type by hand, but then every new field means changing two types. A mapped type builds it from the form for you:
type SignupForm = { name: string; email: string; age: number }; type Touched = { [Field in keyof SignupForm]: boolean };// ^?const touched: Touched = { name: true, email: false, age: false };console.log(touched.name);Line 1 is the form's type. Line 3 is the mapped type. Read it as: "for each Field in the keys of SignupForm, make a property called Field whose type is boolean". The query on line 4 shows the result: { name: boolean; email: boolean; age: boolean }. Line 5 makes a touched object of that type, and line 6 prints true.
It works like a loop over an object's keys in JavaScript (the Object.keys, values, entries lesson covers Object.keys). Here is that loop, building the touched object while the page runs:
const form = { name: "Asha", email: "", age: 30 };
const touched = {};
for (const field of Object.keys(form)) {
touched[field] = false;
}
console.log(touched);Line 1 is a form's values. Line 4 goes through its keys, and line 5 gives each one the value false. Line 7 prints { name: false, email: false, age: false }. The mapped type does the same thing to a type, before the code runs: it goes through the keys and makes one property for each.
SignupForm in turn and makes a property with the same name. The rule after the colon, boolean, is the same for all three, so Touched has three boolean properties.You are making name cards for a dinner. You go down the guest list and, for each guest, write their name on a card with the same printed design. You never invent a guest: the cards follow the list. Add a guest, and you make one more card.
- In real life: The guest list
- In TypeScript: The keys:
keyof SignupForm - In real life: Going down the list, one guest at a time
- In TypeScript:
Field in …takes each key in turn - In real life: The same name card for every guest
- In TypeScript: The type after the colon,
boolean - In real life: A stack of name cards, one per guest
- In TypeScript: The new type,
Touched
A mapped type builds a new object type from a list of keys. { [Key in keyof T]: … } goes through every key of T and makes one property for each, with the type you write after the colon. Change T, and the new type follows.
How [Key in keyof T] walks the keys
A mapped type has three parts. Here is one that shows each key it visits:
type Settings = { theme: string; fontSize: number; showTips: boolean }; type KeyNames = { [Key in keyof Settings]: Key };// ^?keyof Settingsis the union ofSettings's key names:"theme" | "fontSize" | "showTips". (The keyof & typeof lesson coverskeyof.)Key in …takes each of those names in turn.Keyis a name you choose, like the variable of afor…ofloop.- The square brackets
[ ]say "the property's name isKey", and the type after the colon is the property's type.
On line 3 the type after the colon is Key itself, so the query on line 4 shows each property holding its own name: { theme: "theme"; fontSize: "fontSize"; showTips: "showTips" }. Usually you want the property's type instead. Settings[Key] reads it: for "fontSize", it is number. That is an indexed access type, which the Indexed access types lesson covers.
keyof Settings is the list of key names. Key takes each name in turn, like the loop variable of a for…of loop. Settings[Key] reads the type of that property: string, number, then boolean.Now try it. Type a different type after the colon on line 3, and watch the answer after ^? on line 4 and the errors on line 5. It starts as boolean, so line 5's text and number are both errors, on purpose:
type Settings = { theme: string; fontSize: number; showTips: boolean }; type Saved = { [Key in keyof Settings]: boolean };// ^?const saved: Saved = { theme: "dark", fontSize: 16, showTips: true };console.log(saved.fontSize);boolean{ theme: boolean; fontSize: boolean; showTips: boolean }2 errors: line 5Every property becomes a boolean. Line 5 gives theme the text "dark" and fontSize the number 16, so TypeScript reports both: Type 'string' is not assignable to type 'boolean' and Type 'number' is not assignable to type 'boolean'.
With Settings[Key], every property keeps its own type, so Saved is a copy of Settings and the errors are gone. Settings[Key] | null keeps each type and also allows null. string turns every property into text, so the number and the boolean on line 5 are errors.
The keys don't have to come from keyof. Any union of names works, like the names of a site's feature flags:
type Flag = "darkMode" | "newCheckout"; type FlagSettings = { [Name in Flag]: boolean };// ^?const flags: FlagSettings = { darkMode: true, newCheckout: false };console.log(flags.newCheckout);Line 1 lists the flags as a union of literal types. Line 3 maps over that union directly: Name takes "darkMode", then "newCheckout". The query on line 4 shows { darkMode: boolean; newCheckout: boolean }. Line 5 sets both, and line 6 prints false. Add a third name to line 1, and line 5 gets an error until it sets that flag too.
Adding and removing ? and readonly
A property can have two modifiers: ? makes it optional, and readonly stops code from changing it (Object types covers both). A mapped type can add them to every property at once. A profile update, where the user changes only some fields, needs every field optional:
type Profile = { name: string; bio: string }; type ProfileUpdate = { [Key in keyof Profile]?: Profile[Key] };// ^?const update: ProfileUpdate = { bio: "Loves green tea" };console.log(update.bio);Line 3 has ? after the square brackets, so every property is optional. The query on line 4 shows { name?: string | undefined; bio?: string | undefined }. TypeScript adds | undefined because reading a property that was left out gives undefined. Line 5 sends only a new bio, and line 6 prints Loves green tea.
readonly goes before the square brackets. Saved settings shouldn't change by mistake, so here every property is read-only, and line 5 is an error on purpose:
type Settings = { theme: string; fontSize: number }; type FrozenSettings = { readonly [Key in keyof Settings]: Settings[Key] };const saved: FrozenSettings = { theme: "dark", fontSize: 16 };saved.fontSize = 18;console.log(saved.fontSize);Line 5 gets Cannot assign to 'fontSize' because it is a read-only property. It means the type says nobody may change fontSize after the object is made. Notice that line 6 still prints 18 if you run it: the types are removed before the code runs, so readonly doesn't freeze the object in the browser.
A minus sign takes a modifier away: -? makes every property required, and -readonly makes every property changeable. Try each one on a profile whose bio is optional:
type Profile = { name: string; bio?: string }; type Changed = { [Key in keyof Profile]: Profile[Key] };// ^?const update: Changed = { name: "Asha" };update.name = "Ravi";console.log(update.name);Ravi{ name: string; bio?: string | undefined }No errorsNo modifier, so each property keeps the ones it had: name is required and bio stays optional. Line 5 leaves out the bio, which is allowed, and line 6 changes the name. No errors.
Copy as it is keeps bio optional: a mapped type over keyof Profile copies the modifiers each property already had. Add ? makes name optional too. Add readonly makes line 6 an error. Remove ? makes bio required, so line 5, which has no bio, is an error. Every version prints Ravi when it runs.
Profile has one optional property, bio. Adding ? makes both optional. Writing -? takes the ? away, so both are required. readonly and -readonly work the same way.Here is -readonly, turning a saved order into a draft you can edit:
type SavedOrder = { readonly id: number; readonly total: number }; type OrderDraft = { -readonly [Key in keyof SavedOrder]: SavedOrder[Key] };// ^?const draft: OrderDraft = { id: 7, total: 9 };draft.total = 12;console.log(draft.total);Line 1's order has two read-only properties. Line 3 writes -readonly before the square brackets, and the query on line 4 shows { id: number; total: number } with no readonly left. Line 6 changes the total without an error, and line 7 prints 12.
The table below lists the five ways to write it. Read each row as: what you write, what it does, and an example.
| What you write | What it does | For example |
|---|---|---|
[Key in keyof T]: T[Key] | Copies each property, keeping the ? and readonly it already had. | bio?: string stays optional. |
[Key in keyof T]?: T[Key] | Makes every property optional. | A profile update can leave out any field. |
[Key in keyof T]-?: T[Key] | Makes every property required. | bio?: string becomes bio: string. |
readonly [Key in keyof T]: T[Key] | Makes every property read-only. | Saved settings can't be changed by mistake. |
-readonly [Key in keyof T]: T[Key] | Takes readonly away from every property. | A draft of a saved order can be edited. |
Renaming keys with as
So far each new property kept its key's name. Add as after the keys, and you choose a new name for each one. A settings page needs a label for every setting:
type Settings = { theme: string; fontSize: number }; type SettingLabels = { [Key in keyof Settings as `${Key}Label`]: string };// ^?const labels: SettingLabels = { themeLabel: "Theme", fontSizeLabel: "Font size" };console.log(labels.fontSizeLabel);On line 3, as `${Key}Label` says what each key is called in the new type. The backticks make a template literal type: like a JavaScript template literal, but it builds a string type. For "theme" it gives "themeLabel". The next lesson, Template literal types, covers them. The query on line 4 shows { themeLabel: string; fontSizeLabel: string }. Line 5 fills in both labels, and line 6 prints Font size.
With as, the union after in doesn't even have to be key names. Here it is a union of objects, and as turns each one into a key. An app has two kinds of events, and you want one handler function for each kind, named after it:
type ClickEvent = { kind: "click"; x: number; y: number };type SearchEvent = { kind: "search"; query: string };type AppEvent = ClickEvent | SearchEvent; type Handlers = { [Event in AppEvent as Event["kind"]]: (event: Event) => void };// ^?const handlers: Handlers = { click: (event) => console.log(`Clicked at ${event.x}`), search: (event) => console.log(`Searching for ${event.query}`),};handlers.search({ kind: "search", query: "tea" });Lines 1 and 2 describe a click and a search, each with a kind. Line 3 puts them in one union (the Discriminated unions lesson uses the same pattern). On line 5, Event takes each event type in turn, and as Event["kind"] names the property after its kind. The handler's type uses the whole event. The query on line 6 shows { click: (event: ClickEvent) => void; search: (event: SearchEvent) => void }. So on line 9, TypeScript knows event has a query. Line 11 calls the search handler, which prints Searching for tea.
Leaving keys out
as can also leave a key out: if the new name is never, the type that has no values, that property isn't made. A form for a new product has no id yet, because the server gives it one. Press the buttons to change the rule on line 2:
type Product = { id: number; name: string; price: number };type Dropped<Key> = Key extends "id" ? never : Key; type NewProduct = { [Key in keyof Product as Dropped<Key>]: Product[Key] };// ^?const draft: NewProduct = { name: "Tea", price: 3 };console.log(draft.name);name, priceNo errorsprints TeaFor "id", line 2 gives never, and a key that becomes never is left out. NewProduct has only name and price, so line 6's draft, which has no id yet, is fine.
Line 2 is a conditional type: Key extends "id" ? never : Key means "if the key is "id", give never; otherwise give the key back". Line 4 runs it on each key with as Dropped<Key>. With Drop id, the query on line 5 shows { name: string; price: number }. With Keep every key, the draft on line 6 needs an id. With Drop id and price, the draft's price is one key too many. The code prints Tea every time.
as rule runs once for each key. For "id" it gives never, and a key that becomes never is left out. "name" and "price" pass through, so NewProduct has two properties.The rule can look at each property's type instead of its name. A search box only searches text fields:
type Product = { id: number; name: string; price: number; description: string }; type TextFields = { [Key in keyof Product as Product[Key] extends string ? Key : never]: Product[Key];};type SearchKey = keyof TextFields;// ^?Line 4 keeps a key when its property's type, Product[Key], is a string, and turns it into never otherwise. id and price are numbers, so they are left out. Line 6 asks for the keys that are left, and the query on line 7 shows "name" | "description".
Typing a form's state
A real form keeps three things for its fields: the values, which fields were touched, and an error message for each field. Two mapped types make the last two from the form's type, so the form's fields are written in one place. Press the buttons to change the form on line 1:
type SignupForm = { name: string; email: string; age: number }; type FormErrors<Form> = { [Field in keyof Form]?: string };type FormTouched<Form> = { [Field in keyof Form]: boolean }; const values: SignupForm = { name: "Asha", email: "", age: 30 };const touched: FormTouched<SignupForm> = { name: true, email: true, age: false };const errors: FormErrors<SignupForm> = {}; if (touched.email && values.email === "") { errors.email = "Enter your email";}console.log(errors.email);Enter your emailNo errorsFormTouched gives every field a boolean and FormErrors gives every field an optional message. The email was touched and is empty, so line 11 stores a message and line 13 prints Enter your email.
Line 3 gives every field an optional error message, and line 4 gives every field a boolean. Both take the form as a type parameter, Form, so they work for any form (Generic types & interfaces covers types with type parameters). Lines 6 to 8 are the form's state. Line 10 checks whether the email was touched and is still empty, and line 11 stores a message. Line 13 prints Enter your email.
With Add a phone field, both values and touched need a phone, and TypeScript points at lines 6 and 7. With Rename email to mail, every line that still says email gets an error, with the fix in the message. Without mapped types, you would have three types to keep in step by hand.
const flags: { [Flag in "dark" | "beta"]: boolean } = { dark: true };const flags: { [Flag in "dark" | "beta"]?: boolean } = { dark: true };const saved: { readonly [Key in "theme"]: string } = { theme: "dark" }; saved.theme = "light";const draft: { -readonly [Key in "theme"]: string } = { theme: "dark" }; draft.theme = "light";const labels: { [Key in "theme" as `${Key}Label`]: string } = { theme: "Theme" };const labels: { [Key in "theme" as `${Key}Label`]: string } = { themeLabel: "Theme" };const product: { [Key in "id" | "name" as Key extends "id" ? never : Key]: string } = { name: "Tea" };const product: { [Key in "id" | "name" as Key extends "id" ? never : Key]: string } = { id: "7", name: "Tea" };
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
readonly from a mapped type only covers the top level. A nested object can still change:
type Settings = { theme: string; layout: { sidebar: boolean } };type FrozenSettings = { readonly [Key in keyof Settings]: Settings[Key] }; const settings: FrozenSettings = { theme: "dark", layout: { sidebar: true } };settings.layout.sidebar = false;console.log(settings.layout.sidebar);Line 2 makes theme and layout read-only. Line 5 doesn't replace layout; it changes a property inside it, and that inner sidebar isn't read-only. So there is no error, and line 6 prints false. Making every level read-only needs a mapped type that calls itself, which the Writing your own utility types lesson builds.
A mapped type is also different from an index signature. Here are both, with a misspelled key, an error on purpose:
type Settings = { theme: string; fontSize: number }; type AnyFlags = { [key: string]: boolean };type SettingFlags = { [Key in keyof Settings]: boolean }; const loose: AnyFlags = { theme: true, colour: true };const exact: SettingFlags = { theme: true, colour: true };Line 3 is an index signature: any text key may hold a boolean, so line 6's colour is fine. Line 4 is a mapped type with exactly the keys of Settings. Line 7 gets Object literal may only specify known properties, and 'colour' does not exist in type 'SettingFlags': the key is misspelled. fontSize is missing too, but TypeScript reports only the extra key here.
- "A mapped type copies my object while the page runs." No. It builds a type, before the code runs. To build an object, you still write code, like the loop in the overview.
- "readonly in a mapped type freezes the object." No. It only makes changing it a type error. The frozen settings example still prints
18when it runs. - "readonly reaches every level." No. It covers the properties of the top object only.
- "Copying a type with
[Key in keyof T]drops its?." No. A mapped type overkeyof Tkeeps each property's?andreadonly. Use-?or-readonlyto take them away. - "A key that maps to never stays, with the type never." No. With
as, a key that becomesneveris left out. A property whose type isnever, after the colon, would stay. - "I have to write these myself every time." No. TypeScript has
Partial,Required,Readonly,Pick,Omit, andRecordbuilt in, and they are built from mapped types. The Partial, Pick, Omit & friends lesson covers them.
The table below compares the two from the example above. Read each row as a question, then the mapped type's answer, then the index signature's.
| Question | Mapped type: { [Key in keyof Settings]: boolean } | Index signature: { [key: string]: boolean } |
|---|---|---|
| Which keys may the object have? | Exactly the keys of Settings: theme and fontSize. | Any text key at all. |
| Must every key be there? | Yes, unless the mapped type adds ?. | No. It may have none of them. |
What does a misspelled key like colour do? | An error: colour isn't one of the keys. | Nothing. colour is just one more key. |
| Where do the keys come from? | From another type, through keyof, or from a union like "darkMode" | "newCheckout". | It doesn't list keys. [key: string] allows any text key. |
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code: the errors update as you type, and a // ^? line under a type name shows what TypeScript made. Try a guess first, then use the hints.
What type is FontSize? Guess first, then add a // ^? line under it to check.
type Settings = { theme: string; fontSize: number; showTips: boolean }; type AsText = { [Key in keyof Settings]: string };type FontSize = AsText["fontSize"];type Settings = { theme: string; fontSize: number; showTips: boolean }; type AsText = { [Key in keyof Settings]: string };type FontSize = AsText["fontSize"];// ^?Every property of AsText has the type after the colon, string. So AsText["fontSize"] is string, even though fontSize is a number in Settings.
Which property of Copy is optional?
type Profile = { name: string; bio?: string; age: number }; type Copy = { [Key in keyof Profile]: Profile[Key] };type Profile = { name: string; bio?: string; age: number }; type Copy = { [Key in keyof Profile]: Profile[Key] };// ^?Copy keeps the modifiers Profile had, so bio stays optional: the query shows bio?: string | undefined.
Touched is unknown, so line 5 can't read touched.address. Write Touched as a mapped type. What does the code print then?
type CheckoutForm = { address: string; card: string };type Touched<Form> = unknown; const touched: Touched<CheckoutForm> = { address: true, card: false };console.log(touched.address);type CheckoutForm = { address: string; card: string };type Touched<Form> = { [Field in keyof Form]: boolean }; const touched: Touched<CheckoutForm> = { address: true, card: false };console.log(touched.address);With { [Field in keyof Form]: boolean }, Touched<CheckoutForm> is { address: boolean; card: boolean }, so line 5 may read touched.address. It prints true.
The bio hasn't loaded yet, so it is null, and line 4 has an error. Write Nullable so every property may also be null. What type is bio then?
type Profile = { name: string; bio: string };type Nullable<T> = T; const profile: Nullable<Profile> = { name: "Asha", bio: null };console.log(profile.bio);type Profile = { name: string; bio: string };type Nullable<T> = { [Key in keyof T]: T[Key] | null }; const profile: Nullable<Profile> = { name: "Asha", bio: null };type Bio = Nullable<Profile>["bio"];// ^?console.log(profile.bio);{ [Key in keyof T]: T[Key] | null } keeps each type and adds null, so bio: null is allowed. Nullable<Profile>["bio"] is string | null.
The code wants keys like emailError, but SignupErrors has email. Rename the keys on line 3. Which key holds the email's message then?
type SignupForm = { name: string; email: string }; type SignupErrors = { [Field in keyof SignupForm]?: string };const errors: SignupErrors = { emailError: "Enter your email" };console.log(errors.emailError);type SignupForm = { name: string; email: string }; type SignupErrors = { [Field in keyof SignupForm as `${Field}Error`]?: string };// ^?const errors: SignupErrors = { emailError: "Enter your email" };console.log(errors.emailError);The template ${Field}Error after as renames each key, so SignupErrors is { nameError?: string; emailError?: string } and line 5's emailError is allowed. It prints Enter your email.
To save a cart, you only want its data, not its functions. Line 4 has an error because DataOnly still has checkout and clear. Leave out every key whose property is a function. Which keys are left?
type Cart = { items: string[]; total: number; checkout: () => void; clear: () => void }; type DataOnly = { [Key in keyof Cart]: Cart[Key] };const saved: DataOnly = { items: ["Tea"], total: 3 };console.log(saved.total);type Cart = { items: string[]; total: number; checkout: () => void; clear: () => void };type NotAFunction<Key, Value> = Value extends (...args: any[]) => any ? never : Key; type DataOnly = { [Key in keyof Cart as NotAFunction<Key, Cart[Key]>]: Cart[Key] };// ^?const saved: DataOnly = { items: ["Tea"], total: 3 };console.log(saved.total);NotAFunction gives never for checkout and clear, so they are left out. DataOnly is { items: string[]; total: number }, line 6 is fine, and it prints 3.
Check your understanding
For each question, go through the keys one at a time: what is the new key called, is it left out, and what type and modifiers does it get? The quiz code isn't checked on the page, so no error list or type hint gives the answer away.
Question 1 of 7In
{ [Key in keyof Settings]: boolean }, what doesKey in keyof Settingsdo?Choose an answer to see the explanation.
Question 2 of 7What type does
fontSizehave inSaved?Read the code, then predicttype Settings = { theme: string; fontSize: number }; type Saved = { [Key in keyof Settings]: boolean };Choose an answer to see the explanation.
Question 3 of 7Is
biooptional inComplete?Read the code, then predicttype Profile = { name: string; bio?: string }; type Complete = { [Key in keyof Profile]-?: Profile[Key] };Choose an answer to see the explanation.
Question 4 of 7Does TypeScript report an error here, and what does the code print?
Read the code, then predicttype Settings = { fontSize: number }; const saved: { readonly [Key in keyof Settings]: Settings[Key] } = { fontSize: 16 }; saved.fontSize = 18; console.log(saved.fontSize);Choose an answer to see the explanation.
Question 5 of 7Which of these is a key of
Labels?Read the code, then predicttype Settings = { theme: string; fontSize: number }; type Labels = { [Key in keyof Settings as `${Key}Label`]: string };Choose an answer to see the explanation.
Question 6 of 7Which keys does
Listingkeep?Read the code, then predicttype Product = { id: number; name: string; price: number }; type Listing = { [Key in keyof Product as Key extends "id" ? never : Key]: Product[Key] };Choose an answer to see the explanation.
Question 7 of 7How is
{ [Key in keyof Settings]: boolean }different from{ [key: string]: boolean }?Choose an answer to see the explanation.
Key takeaways
{ [Key in keyof T]: … }goes through every key ofTand makes one property for each.- The type after the colon is each property's type.
T[Key]keeps the original type. ?andreadonlyadd modifiers to every property;-?and-readonlytake them away. A plain copy keeps the ones it had.asgives each key a new name. A key that becomesneveris left out.- Mapped types only change types. They don't copy, freeze, or check objects while the page runs.
- Build related types from one source type, and they all follow it when it changes.
Remember the one-liner.
A mapped type goes down a list of keys and makes one property for each, with the name, type, and modifiers you choose.
Coming next: Template literal types. They build string types from other types, like the themeLabel keys in this lesson, and event names like onClick.