Writing your own utility types
Rebuild Partial, Readonly, and Pick from mapped types, write recursive types and a DeepPartial for nested settings, and keep your own helper types readable.
- 01Rebuild the built-in helpersWrite
Partial,Readonly,Pick, andOmityourself from mapped types,keyof, and constraints. - 02Write recursive typesDescribe trees and nested data with a type that refers to itself, and build
DeepPartialandDeepReadonly. - 03Keep helpers readableFlatten what a hover shows, and know the limits of deep helpers, like functions inside an object.
The built-in helpers are types you can write
Partial, Pick, Readonly, and the other helpers from the Partial, Pick, Omit & friends lesson aren't magic. Each one is a short type alias, a name for a type, written in a file that comes with TypeScript. They are built from the same parts you already know: mapped types and conditional types. So you can write them yourself:
type MyPartial<T> = { [Key in keyof T]?: T[Key];}; type Profile = { name: string; email: string; age: number };type ProfileChange = MyPartial<Profile>;// ^?const change: ProfileChange = { email: "asha@example.com" };console.log(change);Lines 1 to 3 make MyPartial. T is a type parameter: a name for whatever type you pass in the angle brackets. Line 2 is a mapped type: for each Key in keyof T, make a property with that name, add ? so it may be left out, and give it the type T[Key], the same type it had in T.
Line 5 describes a profile. Line 6 uses the helper, and the query on line 7 shows every property with a ?. TypeScript also writes | undefined after each type, because a property that is left out reads as undefined. Line 8 gives only an email, and line 9 prints { email: 'asha@example.com' }.
Partial is a mapped type in a library file. Your MyPartial on lines 1 to 3 is the same recipe with longer names, and it makes the same type.A cookbook's recipes look fixed until you read one closely: it's just steps you could follow yourself. Copy it onto your own card and it makes the same dish. Change one step and you have a dish the book never printed.
- In real life: A recipe printed in the cookbook
- In TypeScript: A built-in helper like
Partial - In real life: The ingredient you bring
- In TypeScript: The type you pass in,
T - In real life: Copying a recipe onto your own card
- In TypeScript: Writing
MyPartialyourself - In real life: Changing one step to make a new dish
- In TypeScript: Changing
?toreadonlyfor a new helper
A utility type is a type alias with type parameters. The built-in ones are mapped and conditional types in TypeScript's library, and you can write your own the same way: walk the keys, change each property, or call the helper again for each level.
Rebuild Partial, Readonly, and Pick
Most object helpers are the same mapped type with one small change. Press the buttons to swap the helper's body on line 3, and watch what it does to the profile:
type Profile = { name: string; email: string; age?: number }; type Helper<T> = { [Key in keyof T]?: T[Key] }; type Draft = Helper<Profile>;// ^?const draft: Draft = { name: "Asha", email: "asha@example.com" };draft.name = "Ravi";console.log(draft.name);RaviPartialNo errors? after the brackets makes every property optional. This is the recipe of the built-in Partial.
Line 1 is a profile whose age may be missing. Line 5 applies the helper, and the query on line 6 shows the result. Line 7 makes a draft with a name and an email, and line 8 changes the name. Each recipe changes one thing:
?after the brackets adds a question mark to every key:Partial.-?takes them away:Required. Now line 7 is missingage.readonlyin front locks every key:Readonly. Now line 8 can't change the name.T[Key] | nullchanges the value part instead, which makes a helper TypeScript doesn't have.
Notice that age keeps its ? in the Readonly, Nullable, and Plain copy recipes. A mapped type over keyof T copies each property's ? and readonly unless you add or remove them.
Pick walks only some keys. It takes a second type parameter, K, for the keys to keep, and K extends keyof T says they must be real keys of T (the keyof & typeof lesson covers keyof). A wrong key is an error, on purpose here:
type MyPick<T, K extends keyof T> = { [Key in K]: T[Key] }; type Profile = { name: string; email: string; age: number };type Contact = MyPick<Profile, "name" | "email">;// ^?type Phone = MyPick<Profile, "phone">;Line 1 walks Key in K instead of every key, and keeps each type with T[Key]. Line 4 keeps the name and the email, and the query on line 5 shows just those two. Line 6 asks for a phone. TypeScript says Type '"phone"' does not satisfy the constraint 'keyof Profile': phone isn't one of the profile's keys, so it can't be picked.
Omit is built from other helpers: pick every key except the ones you name. Exclude, from the ReturnType, Awaited & more lesson, takes them out of the union of keys. Writing it yourself lets you make it stricter than the built-in one, on purpose here:
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; type User = { id: number; name: string; email: string };type PublicUser = MyOmit<User, "email">;// ^?type Typo = MyOmit<User, "emial">;type BuiltInTypo = Omit<User, "emial">;Line 1 picks the keys of T that are left after excluding K. Line 4 leaves out the email, and the query on line 5 shows the id and name. Line 6 has a typo, "emial", and gets the same error as MyPick, because MyOmit says K extends keyof T. Line 7 makes the same typo with the built-in Omit, and there is no error: its K only has to be some key name, not one of User's.
The table below shows how TypeScript's library writes each helper. Read each row as: the helper, its recipe, and what it does.
| The helper | How lib.es5.d.ts writes it | What it does |
|---|---|---|
Partial<T> | { [P in keyof T]?: T[P] } | Adds a ? to every property. |
Required<T> | { [P in keyof T]-?: T[P] } | Takes every ? away. |
Readonly<T> | { readonly [P in keyof T]: T[P] } | Makes every property read-only. |
Pick<T, K extends keyof T> | { [P in K]: T[P] } | Walks only the keys in K. |
Omit<T, K extends keyof any> | Pick<T, Exclude<keyof T, K>> | Picks every key except the ones in K. |
Helpers TypeScript doesn't have
Once you can read the recipes, you can write helpers for your own app. A common one: a new user has no id or createdAt yet, because the server adds them. So those two keys should be optional, and the rest required:
type User = { id: number; name: string; email: string; createdAt: string }; type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; type NewUser = Optional<User, "id" | "createdAt">;// ^?const asha: NewUser = { name: "Asha", email: "asha@example.com" };console.log(asha.name);Line 3 makes Optional from built-in helpers. Omit<T, K> is everything except those keys, required as before. Partial<Pick<T, K>> is just those keys, made optional. The & joins the two into one type with all the properties (the Intersection types lesson covers &). Line 7 makes a user without an id, and line 8 prints Asha.
It works, but look at the query on line 6. TypeScript shows the recipe, not the result. A later section fixes that. First, one more kind of helper: one that uses itself.
Types that refer to themselves
Some data has the same shape at every level: a menu whose items have their own items, or comments with replies. A type for it can mention itself. That's called a recursive type, like a recursive function, which calls itself (the Recursion lesson covers those):
type MenuItem = { label: string; children: MenuItem[] }; const menu: MenuItem = { label: "Shop", children: [ { label: "Tea", children: [] }, { label: "Cakes", children: [{ label: "Birthday", children: [] }] }, ],};console.log(menu.children[1].children[0].label);Line 1 says a MenuItem has a label and a list of children, and each child is a MenuItem too. Lines 3 to 9 are a shop menu three levels deep, and the one type fits every level. A child with no children of its own has an empty list. Line 10 reads the second child's first child and prints Birthday.
MenuItem: a label, and children that are MenuItems too. The type is written once, and it fits a menu of any depth.A recursive type can also be a union. Here is a type for any value JSON.stringify can save and load again (the Typing fetch & JSON lesson works with JSON). Line 4 is an error on purpose:
type Json = string | number | boolean | null | Json[] | { [key: string]: Json }; const reply: Json = { user: "Asha", tags: ["tea", 3], seen: true };const withDate: Json = { when: new Date() };Line 1 says a JSON value is text, a number, true or false, null, a list of JSON values, or an object whose values are JSON values. Line 3 nests a list inside an object, and it fits. Line 4 puts a Date inside, and TypeScript says Type '{ when: Date; }' is not assignable to type 'Json': a date isn't one of the kinds of value JSON can hold.
The self-reference has to sit inside something, like an object property or a list, so TypeScript can look at one level at a time. A type that is directly itself is an error, on purpose here:
type Price = Price | number;To work out Price, TypeScript would first need to know Price. It says Type alias 'Price' circularly references itself. You met the other kind of recursive type, a conditional type that calls itself, as UnwrapAll in the infer keyword lesson.
DeepPartial and DeepReadonly
Partial and Readonly only change the top level of an object. A settings object with groups inside needs a helper that goes into every level. Combine a mapped type, a conditional type, and recursion:
type DeepPartial<T> = { [Key in keyof T]?: T[Key] extends object ? DeepPartial<T[Key]> : T[Key];}; type Settings = { theme: { mode: "light" | "dark"; accent: string }; alerts: { email: boolean; push: boolean };}; type ThemeChange = DeepPartial<Settings["theme"]>;// ^?Line 2 does the work. For each key, it makes the property optional, then asks a question about its type: T[Key] extends object, "is this property an object?" If yes, the answer is DeepPartial<T[Key]>: the same helper again, one level down. If no, like a string or a boolean, the type stays as it is, and the recursion stops there.
Lines 5 to 8 describe settings with two groups. Line 10 applies the helper to the theme group, and the query on line 11 shows that the group's own properties are optional now. Press the buttons to compare it with Partial on a whole change:
type DeepPartial<T> = { [Key in keyof T]?: T[Key] extends object ? DeepPartial<T[Key]> : T[Key];}; type Settings = { theme: { mode: "light" | "dark"; accent: string }; alerts: { email: boolean; push: boolean };}; const change: Partial<Settings> = { theme: { mode: "dark" } };console.log(JSON.stringify(change));{"theme":{"mode":"dark"}}Partial1 error: line 10Partial makes only theme optional. Once you give a theme, it must be a whole one: Property 'accent' is missing in type '{ mode: "dark"; }'.
With Partial, line 10 gets an error, because a theme you give must still have every property. With DeepPartial, you can give just the part that changed. What you do give is still checked: "blue" isn't a mode.
theme optional. Only DeepPartial also goes inside it, so mode and accent become optional too.DeepReadonly is the same shape with readonly instead of ?. Line 8 and line 9 are errors on purpose:
type DeepReadonly<T> = { readonly [Key in keyof T]: T[Key] extends object ? DeepReadonly<T[Key]> : T[Key];}; type Settings = { theme: { mode: "light" | "dark" }; tags: string[] }; const settings: DeepReadonly<Settings> = { theme: { mode: "light" }, tags: ["new"] };settings.theme.mode = "dark";settings.tags.push("sale");console.log(settings.theme.mode, settings.tags);With the built-in Readonly, line 8 would be allowed: it only locks the top level (the Mapped types lesson shows this). With DeepReadonly, TypeScript says Cannot assign to 'mode' because it is a read-only property. It even goes into the list: a mapped type over an array type makes a read-only array, so line 9 gets Property 'push' does not exist on type 'readonly string[]'. Like every type, it doesn't freeze anything while the page runs: line 10 still prints dark [ 'new', 'sale' ].
Keeping types readable
A helper is only useful if people can read what it makes. The Optional helper showed its recipe in the hover, not its result. The plain copy recipe from the playground fixes that:
type User = { id: number; name: string; email: string; createdAt: string }; type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;type Prettify<T> = { [Key in keyof T]: T[Key] }; type NewUser = Optional<User, "id" | "createdAt">;// ^?type NewUserFlat = Prettify<NewUser>;// ^?Line 4 makes Prettify, a mapped type that copies every key with its type and modifiers. It changes nothing about which values fit. But to show a mapped type, TypeScript has to list its keys, so the query on line 9 shows one flat object: the name and the email required, the id and createdAt optional. The query on line 7, without it, shows the recipe. You can also put it inside the helper, so every use is flat: Prettify<Omit<T, K> & Partial<Pick<T, K>>>.
A few habits keep your own helpers easy to read and to trust:
- Reach for a built-in helper first. Write your own only when none fits.
- Give each helper a plain name that says what it makes, like
OptionalorDeepPartial, and type parameter names that say what they hold, likeKey. - Build big helpers from small ones, as
OptionalusesOmit,Pick, andPartial. - Check each helper with a
// ^?line on a real type, and try one value that should fail. - Only go deep when your data is deep. A recursive helper makes every hover and error message longer.
Saving part of a nested settings object
A settings page saves only what the reader changed: maybe the theme's mode and one alert. DeepPartial types that change, and the function fills in the rest from the saved settings:
type DeepPartial<T> = { [Key in keyof T]?: T[Key] extends object ? DeepPartial<T[Key]> : T[Key];}; type Settings = { theme: { mode: "light" | "dark"; accent: string }; alerts: { email: boolean; push: boolean };}; function updateSettings(current: Settings, change: DeepPartial<Settings>): Settings { return { theme: { ...current.theme, ...change.theme }, alerts: { ...current.alerts, ...change.alerts }, };} const saved: Settings = { theme: { mode: "light", accent: "orange" }, alerts: { email: true, push: true },};const updated = updateSettings(saved, { theme: { mode: "dark" }, alerts: { push: false } });console.log(updated.theme.mode, updated.theme.accent, updated.alerts.push);Lines 1 to 8 are the helper and the settings from before. Line 10 says updateSettings takes the current settings and a change of type DeepPartial<Settings>, and gives back whole Settings. Lines 12 and 13 copy each group and then lay the changed parts over it with the spread syntax ... (the Spread and rest lesson covers it). Lines 17 to 20 are the saved settings. Line 21 changes only the mode and push alerts, and line 22 prints dark orange false: the accent was kept.
type P<T> = { [K in keyof T]?: T[K] };type P<T> = { [K in T]: string };type Pk<T, K> = { [P in K]: T[P] };type Pk<T, K extends keyof T> = { [P in K]: T[P] };type Price = Price | number;type Tree = { value: number; children: Tree[] };type Nested = Nested[];type Box<T> = { readonly [K in keyof T]: Box<T[K]> };
Sort each helper by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
T[Key] extends object is true for more than plain objects. Arrays are objects, and so are functions. For arrays that works out, as you saw. For a function, it doesn't, and line 15 is an error on purpose:
type DeepReadonly<T> = { readonly [Key in keyof T]: T[Key] extends object ? DeepReadonly<T[Key]> : T[Key];}; type Cart = { items: { name: string; price: number }[]; checkout: (total: number) => string;}; function lock(cart: Cart): DeepReadonly<Cart> { return cart;} const cart = lock({ items: [{ name: "Tea", price: 3 }], checkout: (total) => `Paid ${total}` });console.log(cart.checkout(3));Line 7 gives the cart a checkout function. DeepReadonly sees an object and turns it into a mapped type, and a mapped type can't be called. Line 15 gets This expression is not callable. The code still runs and prints Paid 3: only the type is wrong. The fix is to check for a function first and leave it as it is:
type DeepReadonly<T> = { readonly [Key in keyof T]: T[Key] extends (...args: never[]) => unknown ? T[Key] : T[Key] extends object ? DeepReadonly<T[Key]> : T[Key];}; type Cart = { items: { name: string; price: number }[]; checkout: (total: number) => string;}; function lock(cart: Cart): DeepReadonly<Cart> { return cart;} const cart = lock({ items: [{ name: "Tea", price: 3 }], checkout: (total) => `Paid ${total}` });console.log(cart.checkout(3));Line 2 asks first whether the property is a function: (...args: never[]) => unknown is a type every function fits. If it is, line 3 keeps it. Only otherwise do lines 4 to 6 go deeper. Now line 19 type checks and prints Paid 3.
- "Utility types are built into the compiler." No.
Partial,Pick,Omit, and friends are type aliases in a library file. You can read them and write your own. Only a few, like theUppercasefrom Template literal types, are built into the compiler. - "A mapped type always drops
?andreadonly." No. Walkingkeyof Tkeeps each property's modifiers unless you write-?or-readonly. - "
Pick<T, K>works withoutK extends keyof T." No. Without it,Kcould be anything, andT[Key]might not exist. - "
Partialmakes nested objects optional too." No. It changes only the top level. Use a recursiveDeepPartial. - "A type can refer to itself any way it likes." No. The self-reference must sit inside an object, a list, or a conditional type, not be the whole type.
- "A deep helper is safe on any object." No. Functions are objects too. Check for them first.
- "
Prettifychanges the type." No. It changes only how the type is shown.
The table below sets the built-in helpers beside the deep ones. Read each row as: the helper, how far it goes, and what that means.
| The helper | How far it goes | What that means |
|---|---|---|
Partial<Settings> | Only the top-level properties. | A theme you give must still be a whole theme. |
DeepPartial<Settings> | Every level, all the way down. | A theme with only a mode is fine. |
Readonly<Settings> | Only the top-level properties. | settings.theme.mode = "dark" is allowed. |
DeepReadonly<Settings> | Every level, including arrays. | settings.theme.mode = "dark" is an error. |
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code: the errors and ^? answers update as you type. Try a guess first, then use the hints.
What type does title have in TodoDraft? Guess first. Then press Edit & run and add a // ^? line under line 4 to check.
type Nullable<T> = { [Key in keyof T]: T[Key] | null }; type Todo = { title: string; done: boolean };type TodoDraft = Nullable<Todo>;type Nullable<T> = { [Key in keyof T]: T[Key] | null }; type Todo = { title: string; done: boolean };type TodoDraft = Nullable<Todo>;// ^?Each property keeps its type and gets | null, so title is string | null and done is boolean | null. The query on line 5 shows both.
Two lines use a hand-written MyPick. Which line will TypeScript mark?
type MyPick<T, K extends keyof T> = { [Key in K]: T[Key] };
type Todo = { title: string; done: boolean };
type Summary = MyPick<Todo, "title">;
type Planned = MyPick<Todo, "title" | "due">;type MyPick<T, K extends keyof T> = { [Key in K]: T[Key] }; type Todo = { title: string; done: boolean };type Summary = MyPick<Todo, "title">;type Planned = MyPick<Todo, "title" | "done">;Todo has title and done. Line 5 asks for due, so K extends keyof T fails. Picking "done" instead fixes it.
Line 5 changes a todo, and TypeScript allows it. Change line 1 so MyReadonly works like Readonly and line 5 is marked.
type MyReadonly<T> = { [Key in keyof T]: T[Key] }; type Todo = { title: string; done: boolean };const todo: MyReadonly<Todo> = { title: "Buy tea", done: false };todo.done = true;console.log(todo.done);type MyReadonly<T> = { readonly [Key in keyof T]: T[Key] }; type Todo = { title: string; done: boolean };const todo: MyReadonly<Todo> = { title: "Buy tea", done: false };todo.done = true;console.log(todo.done);readonly [Key in keyof T] makes every property read-only, so line 5 gets Cannot assign to 'done' because it is a read-only property.
ValueOf<Settings> should be the type of any one setting's value. Replace unknown on line 1, so line 4, which stores true, is marked.
type ValueOf<T> = unknown; type Settings = { theme: "light" | "dark"; fontSize: number };const value: ValueOf<Settings> = true;type ValueOf<T> = T[keyof T]; type Settings = { theme: "light" | "dark"; fontSize: number };const value: ValueOf<Settings> = true;T[keyof T] looks up every key of T and joins the answers, so ValueOf<Settings> is "light" | "dark" | number. true isn't one of those, so line 4 is marked.
Line 7 moves the order to another city, and TypeScript allows it. Change the type after the colon on line 2 so every level is read-only.
type DeepReadonly<T> = { readonly [Key in keyof T]: T[Key];}; type Order = { id: number; address: { city: string } };const order: DeepReadonly<Order> = { id: 7, address: { city: "Pune" } };order.address.city = "Goa";console.log(order.address.city);type DeepReadonly<T> = { readonly [Key in keyof T]: T[Key] extends object ? DeepReadonly<T[Key]> : T[Key];}; type Order = { id: number; address: { city: string } };const order: DeepReadonly<Order> = { id: 7, address: { city: "Pune" } };order.address.city = "Goa";console.log(order.address.city);T[Key] extends object ? DeepReadonly<T[Key]> : T[Key] goes one level down for objects, so the address is read-only too, and line 7 gets Cannot assign to 'city' because it is a read-only property.
Line 4 gets Object is of type 'unknown', because TypeScript doesn't know what a reply is. Change the type of replies on line 1 so each reply is a comment with replies of its own.
type Comment = { text: string; replies: unknown[] }; const thread: Comment = { text: "Love this tea", replies: [{ text: "Me too", replies: [] }] };console.log(thread.replies[0].text);type Comment = { text: string; replies: Comment[] }; const thread: Comment = { text: "Love this tea", replies: [{ text: "Me too", replies: [] }] };console.log(thread.replies[0].text);With replies: Comment[], each reply is a Comment, so thread.replies[0].text is a string. The error is gone, and line 4 prints Me too.
Check your understanding
For each question, ask: which keys does the helper walk, and what does it do to each property? The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What is
Partialin TypeScript's own library?Choose an answer to see the explanation.
Question 2 of 7With this helper, what is the type of
ageinFull?Read the code, then predicttype Helper<T> = { [Key in keyof T]-?: T[Key] }; type Profile = { name: string; age?: number }; type Full = Helper<Profile>;Choose an answer to see the explanation.
Question 3 of 7Why does TypeScript mark line 4 of this MyPick code?
Read the code, then predicttype MyPick<T, K extends keyof T> = { [Key in K]: T[Key] }; type Profile = { name: string; email: string }; type Phone = MyPick<Profile, "phone">;Choose an answer to see the explanation.
Question 4 of 7Which of these types refers to itself in a way TypeScript accepts?
Choose an answer to see the explanation.
Question 5 of 7Both lines give a theme with only a mode. Which line does TypeScript mark?
Read the code, then predicttype DeepPartial<T> = { [Key in keyof T]?: T[Key] extends object ? DeepPartial<T[Key]> : T[Key]; }; type Settings = { theme: { mode: "light" | "dark"; accent: string } }; const a: Partial<Settings> = { theme: { mode: "dark" } }; const b: DeepPartial<Settings> = { theme: { mode: "dark" } };Choose an answer to see the explanation.
Question 6 of 7What does TypeScript say about the call on line 8?
Read the code, then predicttype DeepReadonly<T> = { readonly [Key in keyof T]: T[Key] extends object ? DeepReadonly<T[Key]> : T[Key]; }; type Cart = { checkout: (total: number) => string }; const plain: Cart = { checkout: (total) => `Paid ${total}` }; const cart: DeepReadonly<Cart> = plain; cart.checkout(3);Choose an answer to see the explanation.
Question 7 of 7What does
Prettify<T> = { [Key in keyof T]: T[Key] }change about a type?Choose an answer to see the explanation.
Key takeaways
- Utility types are type aliases with type parameters.
Partialis{ [P in keyof T]?: T[P] }in TypeScript's library. - Most object helpers are one mapped type with one change:
?,-?,readonly, or a new value type. Walkingkeyof Tkeeps each property's modifiers. K extends keyof Tkeeps a helper's keys real, which makes your ownOmitstricter than the built-in one.- A recursive type refers to itself inside an object, a list, or a conditional type, to describe trees and nested data.
DeepPartialandDeepReadonlycall themselves for each property that is an object. Check for functions first.Prettify, a plain copy mapped type, makes a hover show the result instead of the recipe.
Remember the one-liner.
A utility type is a recipe: walk the keys, change each property, and call it again for the next level.
Coming next: Modules & type imports. It shares types between files with import and export, so a helper like DeepPartial is written once for the whole app.