Partial, Pick, Omit & friends
Use TypeScript's built-in helpers Partial, Required, Readonly, Pick, Omit, and Record to make new object types, like a profile update sent from a form.
- 01Type changes and draftsMake every property optional with
Partialor required withRequired, and type a form update with them. - 02Keep or leave out propertiesBuild smaller types with
PickandOmit, and know which of the two checks the names you give it. - 03Lock and map with Readonly and RecordStop changes with
Readonly, build lookup objects withRecord, and know what neither checks while the page runs.
New object types from the one you have
A settings page lets a user change their name, email, or age. When they press Save, the page sends only what changed, like a new email. The whole profile has a type already. You need a second type for the change, where every property may be left out. Instead of writing it again by hand, you can make it from the first one:
type Profile = { name: string; email: string; age: number };type ProfileChange = Partial<Profile>;// ^? const wholeProfile: Profile = { email: "asha@shop.example" };const change: ProfileChange = { email: "asha@shop.example" };console.log(change);Line 1 describes a whole Profile, using an object type. Line 2 makes a new type from it: Partial<Profile>. You write the helper's name, then the type you start from in angle brackets. The query on line 3 shows what Partial made: the same three properties, each with a ? after its name. A ? makes a property optional: an object may leave it out. Each type also gained | undefined, because a property you read may not be there.
Line 5 stores a change with only an email as a whole Profile, and TypeScript marks it on purpose: Type '{ email: string; }' is missing the following properties from type 'Profile': name, age. It means a Profile needs a name and an age, and this object has neither. Line 6 stores the same object as a ProfileChange, where every property is optional, so it fits. Line 7 prints { email: 'asha@shop.example' }.
Partial copies every property of Profile and adds a ? to each. The original Profile doesn't change.Partial is one of TypeScript's utility types: helpers, built into TypeScript, that make a new type from one you already have. This lesson covers the six you'll use most for objects. Each one leaves the type you give it as it is. And when you add a property to Profile, every type made from it gets that property too.
You could already write Partial yourself. It is the mapped type { [Key in keyof T]?: T[Key] }, which goes through every key of T and adds a ?. TypeScript builds it in, with a name, so you don't write it again in every project. The other helpers in this lesson are built from mapped types too.
An office keeps one master copy of a form. When it needs a change-of-address slip, it doesn't design a new form. It copies the master and marks every box "only if it changed". For a name badge, it copies just the name box. The master never changes, and every copy matches it.
- In real life: The original form, where every box must be filled in
- In TypeScript:
Profile - In real life: A copy where each box says "fill in only if it changed"
- In TypeScript:
Partial<Profile> - In real life: A copy with only the name and email boxes
- In TypeScript:
Pick<Profile, "name" | "email"> - In real life: A copy printed in ink you can't erase
- In TypeScript:
Readonly<Profile> - In real life: Add a box to the original, and the next copies have it too
- In TypeScript: Add a property to
Profile, and every helper type gets it
A utility type makes a new type from one you have. You give it a type in angle brackets, like Partial<Profile>, and get back a changed copy: every property optional, only some properties, or every property read-only. The original type doesn't change.
Partial and Required
Required does the opposite of Partial: it takes away every ?. A settings form may leave some settings out, but once the page has saved them, every setting has a value:
type Settings = { theme?: "light" | "dark"; fontSize?: number };type SavedSettings = Required<Settings>;// ^? const fromForm: Settings = { theme: "dark" };const saved: SavedSettings = { theme: "dark" };Line 1 says a theme and a font size are both optional. Line 2 makes SavedSettings with Required, and the query on line 3 shows both properties without their ?, and without | undefined. Line 5 is fine: a form may leave the font size out. Line 6 isn't, so TypeScript marks it on purpose: Property 'fontSize' is missing in type '{ theme: "dark"; }' but required in type 'Required<Settings>'.
The usual way to get every setting is to start from defaults, then put the user's choices over them:
type Settings = { theme?: "light" | "dark"; fontSize?: number }; const defaults: Required<Settings> = { theme: "light", fontSize: 16 };const fromForm: Settings = { theme: "dark" }; const saved: Required<Settings> = { ...defaults, ...fromForm };console.log(saved);Line 3 gives every setting a default, so it fits Required<Settings>. Line 4 is what the form sent: only a theme. Line 6 builds the saved settings with the spread syntax: first every default, then the form's choices, which replace the defaults they share a name with. TypeScript works out that every setting now has a value, so the result fits Required<Settings>. Line 7 prints { theme: 'dark', fontSize: 16 }.
The table below compares the two. Read each row as a question, then the answer for each helper.
| Question | Partial<T> | Required<T> |
|---|---|---|
| What does it do to each property? | Makes it optional, as if it had a ?. | Makes it required, as if its ? were taken away. |
| Which objects fit? | Any mix of the properties, even {}. | Only objects with every property. |
| What type does each property have? | Its own type, or undefined. | Its own type, without the undefined that ? added. |
| When do you use it? | For a change, a draft, or a search filter, where some fields are left out. | For settings after the defaults are filled in, where every field is known. |
Pick and Omit
A shop's product has an id, a name, a price, and a description. A small product card shows only the name and the price. A form for a new product has everything except the id, because the server gives each product its id. Pick and Omit make those two types:
type Product = { id: number; name: string; price: number; description: string }; type ProductCard = Pick<Product, "name" | "price">;// ^?type NewProduct = Omit<Product, "id">;// ^? const card: ProductCard = { name: "Tea", price: 3 };console.log(`${card.name}: $${card.price}`);Line 3 uses Pick with two things in the angle brackets: the type, and the names to keep, written as a union like "name" | "price". The query on line 4 shows a type with only those two properties. Line 5 uses Omit, which keeps every property except the names you list, so the query on line 6 shows name, price, and description. Each property keeps its type from Product. Line 8 makes a card, and line 9 prints Tea: $3.
Pick<Product, "name" | "price"> keeps the two names you list. Omit<Product, "id"> keeps everything except id.Now type the names yourself. Change line 3 and watch the answer after the query on line 4:
type Product = { id: number; name: string; price: number; description: string }; type ProductCard = Pick<Product, "name" | "price">;// ^?"name" | "price"{ name: string; price: number; }No errorsBoth names are properties of Product, so ProductCard has exactly those two: name and price.
Any names Product has are fine, and keyof Product, which means "every property name of Product", gives you a full copy. "colour" gets an error, because Pick only takes names the type really has. Omit is different:
type Product = { id: number; name: string; price: number; description: string }; type CardByPick = Pick<Product, "name" | "colour">;type CardByOmit = Omit<Product, "colour">;// ^?Line 3 asks Pick for "colour", and TypeScript marks it on purpose: Type '"name" | "colour"' does not satisfy the constraint 'keyof Product'. In plain words: one of those names isn't a property of Product. Line 4 asks Omit to take out "colour", and there's no error. Omit accepts any name, so a typo goes unnoticed. The query on line 5 shows that nothing was taken out: all four properties are still there.
The table below compares the two. Read each row as a question, then the answer for each helper.
| Question | Pick<T, Keys> | Omit<T, Keys> |
|---|---|---|
| What does it keep? | Only the properties you list. | Every property except the ones you list. |
| What if you list a name the type doesn't have? | An error: the name doesn't satisfy keyof the type. | No error. Nothing is taken out. |
| When do you use it? | When you need a few properties, like a product card. | When you need almost all of them, like a new product without its id. |
Readonly and Record
Readonly marks every property readonly, so your code can read it but not assign a new value to it. A todo item on a page that only shows the list might be read-only:
type Todo = { title: string; done: boolean }; const todo: Readonly<Todo> = { title: "Buy tea", done: false };todo.done = true;console.log(todo.done);Line 3 makes a Readonly<Todo>. Line 4 tries to change done, and TypeScript marks it on purpose: Cannot assign to 'done' because it is a read-only property. But Readonly is only a type. Run the code anyway and line 5 prints true: the change happens, because the types are gone when the code runs. To stop changes while the page runs, use Object.freeze. TypeScript gives what it returns a Readonly type.
Record makes an object type from a list of keys and one value type. You may have met Record<string, number> in the Index signatures lesson. With a union of keys, it needs a property for every one. A Save button shows a different label for each status:
type Status = "idle" | "saving" | "saved";type ButtonLabels = Record<Status, string>;// ^? const labels: ButtonLabels = { idle: "Save", saving: "Saving…",};console.log(labels.saving);Line 1 lists three statuses. Line 2 makes ButtonLabels: one property for each status, each holding a string, as the query on line 3 shows. Lines 5 to 8 give labels for idle and saving, but forget saved, so TypeScript marks line 5 on purpose: Property 'saved' is missing. That's the point of Record: add a fourth status later, and TypeScript points at every object that needs a label for it. Line 9 still prints Saving….
Record<Status, string> needs a string for every status. The example gives idle and saving a label, but saved has none, so TypeScript reports it.With string as the keys, any name is allowed, which suits a lookup like prices. It also means TypeScript can't know which keys are there:
const prices: Record<string, number> = { tea: 3, cake: 4 }; console.log(prices.tea);const coffee = prices.coffee;// ^?console.log(coffee);Line 1 stores two prices. Line 3 prints 3. Line 4 reads coffee, which isn't there. The query on line 5 says const coffee: number, but line 6 prints undefined. With string keys, check that a key exists before you use its value.
Which helper fits?
Here is a draft product with only a name. Put a different helper on line 2, and watch which ones accept the draft on line 4:
type Product = { id: number; name: string; price: number };type Draft = Partial<Product>;// ^?const draft: Draft = { name: "Tea" };console.log(draft);Partial<Product>{ id?: number | undefined; name?: string | undefined; price?: number | undefined; }No errorsPartial makes every property optional, so an object with only a name fits. This is the type for a draft or a change.
Partial and Pick name accept it. Required and Readonly don't make anything optional, so they still need an id and a price. Omit id takes away the id, but the price is still required.
The table below lists all six helpers. Read each row as: this helper, the type it makes, and where you'd use it.
| Helper | What it makes | An everyday use |
|---|---|---|
Partial<T> | Every property optional. | A change from a form, or a draft. |
Required<T> | Every property required. | Settings after the defaults are filled in. |
Readonly<T> | Every property read-only. | Data your code must not change, like a saved order. |
Pick<T, Keys> | Only the listed properties. | A product card with a name and a price. |
Omit<T, Keys> | Every property except the listed ones. | A new product, before the server gives it an id. |
Record<Keys, Type> | One property for each key, all of the same type. | A label for every status of a button. |
Typing a form update
Back to the profile form. The page sends a change, and a function puts it over the saved profile. The change may hold any property except id, which no form should touch. Helpers combine, so you can say exactly that:
type Profile = { id: number; name: string; email: string };type ProfileChange = Partial<Omit<Profile, "id">>; function updateProfile(profile: Profile, changes: ProfileChange): Profile { return { ...profile, ...changes };} const asha: Profile = { id: 7, name: "Asha", email: "asha@example.com" };const updated = updateProfile(asha, { email: "asha@shop.example" });console.log(updated);Line 2 reads from the inside out: Omit<Profile, "id"> takes away the id, then Partial makes what's left optional. Lines 4 to 6 make updateProfile. Line 5 spreads the old profile first and the change over it, so changed properties replace the old ones and the rest stay. Line 9 sends a new email, and line 10 prints { id: 7, name: 'Asha', email: 'asha@shop.example' }.
Now try what a form might really send:
type Profile = { id: number; name: string; email: string };type ProfileChange = Partial<Omit<Profile, "id">>; function updateProfile(profile: Profile, changes: ProfileChange): Profile { return { ...profile, ...changes };} const asha: Profile = { id: 7, name: "Asha", email: "asha@example.com" };const updated = updateProfile(asha, { email: "asha@shop.example" });console.log(updated);{ id: 7, name: 'Asha', email: 'asha@shop.example' }{ email: "asha@shop.example" }No errorsThe form changed only the email. The change has one property, which ProfileChange allows, and the spread puts it over the old email.
The first three changes have no errors. A typo gets Did you mean to write 'email'?: without TypeScript, the profile would get an extra emial property, and the email wouldn't change. Change the id is caught because of the Omit, and A number for the name because an optional name must still be a string when it's there.
Forms also show error messages, one at most for each field. Partial over a Record types them:
type Field = "name" | "email";type FormErrors = Partial<Record<Field, string>>;// ^? const errors: FormErrors = {};errors.email = "Enter an email address";console.log(errors);Line 2 makes a Record with a message for each field, then makes each message optional, as the query on line 3 shows. Line 5 starts with no errors, line 6 adds one for the email, and line 7 prints { email: 'Enter an email address' }.
const change: Partial<{ name: string; age: number }> = {};const saved: Required<{ name?: string }> = {};const card: Pick<{ name: string; age: number }, "age"> = { age: 3 };const card: Pick<{ name: string; age: number }, "email"> = { age: 3 };const card: Omit<{ name: string; age: number }, "email"> = { name: "Tea", age: 3 };const count: Readonly<{ value: number }> = { value: 1 }; count.value = 2;const prices: Record<"tea" | "cake", number> = { tea: 3 };const prices: Record<string, number> = { tea: 3, cake: 4 };
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
Readonly covers only the object's own properties. A list inside it can still change:
type Cart = { owner: string; items: string[] }; const cart: Readonly<Cart> = { owner: "Asha", items: ["Tea"] };cart.items.push("Cake");console.log(cart.items);Line 3 makes a read-only cart. Line 4 adds a cake to its list with push, and there's no error: push doesn't assign a new list to cart.items, it changes the list that's already there. Line 5 prints [ 'Tea', 'Cake' ]. To protect the list too, give items the type readonly string[].
Partial has a surprise of its own. By default, an optional property may be left out or hold undefined:
type Profile = { name: string; email: string }; function updateProfile(profile: Profile, changes: Partial<Profile>): Profile { return { ...profile, ...changes };} const asha: Profile = { name: "Asha", email: "asha@example.com" };const updated = updateProfile(asha, { name: undefined });console.log(updated.name);Line 8 sends { name: undefined }, and TypeScript allows it. The spread on line 4 copies it, so line 9 prints undefined, even though the return type says the name is a string. The compiler option exactOptionalPropertyTypes makes a ? mean only "may be left out". Here is the same code with it turned on:
type Profile = { name: string; email: string }; function updateProfile(profile: Profile, changes: Partial<Profile>): Profile { return { ...profile, ...changes };} const asha: Profile = { name: "Asha", email: "asha@example.com" };const updated = updateProfile(asha, { name: undefined });console.log(updated.name);Now line 8 gets an error on purpose: Argument of type '{ name: undefined; }' is not assignable to parameter of type 'Partial<Profile>' with 'exactOptionalPropertyTypes: true'. A change may leave the name out, but it may not set it to undefined.
The table below lists things the helpers don't stop. Read each row as: this code, whether TypeScript stops it, and what happens when it runs.
| You write | Does TypeScript stop it? | When the code runs |
|---|---|---|
todo.done = true on a Readonly<Todo> | Yes, with an error. | The value still changes. The types are gone when the code runs. |
cart.items.push("Cake") on a Readonly<Cart> | No. Readonly covers only the cart's own properties. | The list gets a new item. |
updateProfile(asha, { name: undefined }) with Partial<Profile> | No, unless the exactOptionalPropertyTypes option is on. | The name becomes undefined. |
prices.coffee on a Record<string, number> | No. Every string key is allowed. | You get undefined, though the type says number. |
- "
Partial<Profile>changes theProfiletype." No. It makes a new type.Profilestays exactly as it was. - "
Readonlyfreezes the object." No. It's only a type, so the object can still change when the code runs.Object.freezeis what stops changes while the page runs. - "
Readonlyprotects everything inside." No. It covers the object's own properties. A list or object inside can still change. - "
Omitwarns me about a wrong name." No.Omitaccepts any name.Pickis the one that checks. - "An optional property can only be left out." No. By default it may also hold
undefined, unlessexactOptionalPropertyTypesis on. - "
Partialmakes nested objects optional too." No. It changes only the top level. Anaddressthat is there must still be a whole address.
Practice exercises
Each exercise is TypeScript. To change the starter code, press Edit & run: the errors update as you type, and a // ^? line under a type shows what a helper made. Try a guess first, then use the hints.
Which properties must an object of type SettingsChange have? Guess first, then add a // ^? line under line 2 to check.
type Settings = { theme: string; fontSize: number };type SettingsChange = Partial<Settings>;type Settings = { theme: string; fontSize: number };type SettingsChange = Partial<Settings>;// ^?const noChange: SettingsChange = {};Every property of SettingsChange is optional, so it needs none of them: line 4 stores {} with no error. The query on line 3 shows theme? and fontSize?.
A saved order is read-only. Which line will TypeScript mark?
type Order = { id: number; status: string };
const order: Readonly<Order> = { id: 7, status: "paid" };
console.log(order.status);
order.status = "shipped";type Order = { id: number; status: string }; const order: Readonly<Order> = { id: 7, status: "paid" };console.log(order.status);const shipped: Readonly<Order> = { ...order, status: "shipped" };console.log(shipped.status);Line 5 assigns to order.status, which Readonly made read-only, so TypeScript reports Cannot assign to 'status'. Instead of changing the order, the solution makes a new one with the spread syntax, and prints paid, then shipped.
TypeScript marks line 5, because a card isn't a whole product. Change only line 3 so that a card needs just a name and a price. What does the fixed code print?
type Product = { id: number; name: string; price: number; description: string }; type ProductCard = Product; const card: ProductCard = { name: "Tea", price: 3 };console.log(`${card.name}: $${card.price}`);type Product = { id: number; name: string; price: number; description: string }; type ProductCard = Pick<Product, "name" | "price">; const card: ProductCard = { name: "Tea", price: 3 };console.log(`${card.name}: $${card.price}`);Pick<Product, "name" | "price"> is { name: string; price: number; }, so the card on line 5 fits, and line 6 prints Tea: $3.
A blog shows a badge for each post status. TypeScript marks line 3. Which key is missing? Add it.
type Status = "draft" | "published" | "archived"; const badges: Record<Status, string> = { draft: "Draft", published: "Live",};console.log(badges.published);type Status = "draft" | "published" | "archived"; const badges: Record<Status, string> = { draft: "Draft", published: "Live", archived: "Archived",};console.log(badges.published);Record<Status, string> needs a property for each of the three statuses. The object was missing archived. With it added, the error is gone, and the code prints Live.
This code changes the profile's id, and TypeScript says nothing. Change line 2 so that a change can't hold an id. Which line gets an error then?
type Profile = { id: number; name: string; email: string };type ProfileChange = Partial<Profile>; function updateProfile(profile: Profile, changes: ProfileChange): Profile { return { ...profile, ...changes };} const asha: Profile = { id: 7, name: "Asha", email: "asha@example.com" };console.log(updateProfile(asha, { id: 8 }));type Profile = { id: number; name: string; email: string };type ProfileChange = Partial<Omit<Profile, "id">>; function updateProfile(profile: Profile, changes: ProfileChange): Profile { return { ...profile, ...changes };} const asha: Profile = { id: 7, name: "Asha", email: "asha@example.com" };console.log(updateProfile(asha, { id: 8 }));With Partial<Omit<Profile, "id">>, a change has no id property, so line 9's { id: 8 } gets an error. The starter printed a profile with the id changed to 8; now TypeScript stops that before the code runs.
TypeScript marks line 5: Type 'number | undefined' is not assignable to type 'number'. Fix it with a set of defaults that has every setting. What does the fixed code print?
type Settings = { theme?: "light" | "dark"; fontSize?: number }; function applySettings(fromForm: Settings) { const saved = { theme: "light", ...fromForm }; const size: number = saved.fontSize; console.log(saved.theme, size);} applySettings({ theme: "dark" });type Settings = { theme?: "light" | "dark"; fontSize?: number }; const defaults: Required<Settings> = { theme: "light", fontSize: 16 }; function applySettings(fromForm: Settings) { const saved = { ...defaults, ...fromForm }; const size: number = saved.fontSize; console.log(saved.theme, size);} applySettings({ theme: "dark" });The defaults have every setting, and the form's choices go over them, so saved.fontSize is always a number. The theme comes from the form, the size from the defaults: it prints dark 16.
Check your understanding
For each question, write out the type the helper makes before you choose. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7Which properties must an object of type
Partial<Profile>have?Choose an answer to see the explanation.
Question 2 of 7What type is
Card?Read the code, then predicttype Product = { id: number; name: string; price: number }; type Card = Pick<Product, "name" | "price">;Choose an answer to see the explanation.
Question 3 of 7Does TypeScript report an error for this Omit, and what is
Card?Read the code, then predicttype Product = { id: number; name: string; price: number }; type Card = Omit<Product, "colour">;Choose an answer to see the explanation.
Question 4 of 7This code has a type error. What does line 4 print when it runs anyway?
Read the code, then predicttype Todo = { title: string; done: boolean }; const todo: Readonly<Todo> = { title: "Buy tea", done: false }; todo.done = true; console.log(todo.done);Choose an answer to see the explanation.
Question 5 of 7What does TypeScript report about
badges?Read the code, then predicttype Status = "draft" | "published" | "archived"; const badges: Record<Status, string> = { draft: "Draft", published: "Live" };Choose an answer to see the explanation.
Question 6 of 7Which object fits
SavedSettings?Read the code, then predicttype Settings = { theme?: string; fontSize?: number }; type SavedSettings = Required<Settings>;Choose an answer to see the explanation.
Question 7 of 7A cart has the type
Readonly<Cart>, and itsitemsis astring[]. What does TypeScript say aboutcart.items.push("Cake")?Choose an answer to see the explanation.
Key takeaways
- A utility type makes a new type from one you have, like
Partial<Profile>. The original doesn't change. Partialmakes every property optional, for changes and drafts.Requiredmakes every property required.Pickkeeps the properties you list, and checks the names.Omitkeeps the rest, and doesn't check.Readonlystops assignments in your code, but nothing while the page runs, and only at the top level.Record<Keys, Type>needs a property for every key. Withstringkeys, any key may be missing.- Helpers combine:
Partial<Omit<Profile, "id">>is a form update that can't touch the id.
Remember the one-liner.
Write the full type once, and let the helpers make its drafts, cards, and lookups for you.
Coming next: ReturnType, Awaited & more. It covers the built-in helpers for unions and functions, like Exclude, ReturnType, and Awaited.