TypeScript Utility Types
Types you already have, reshaped
TypeScript utility types are the built-in generics that take a type you already wrote and hand you a variant of it. Make every field optional. Drop one property. Pull the return type out of a function. They ship with the compiler, so there is nothing to install and nothing to maintain.
The official handbook lists them alphabetically, which is the least useful order. They come in three families — object reshapers, union filters, and function extractors — and once you see the families you stop looking them up. This page walks each family in turn, then shows you how one is built so the rest stop feeling like black boxes.
What TypeScript utility types actually are
Every one of them is a plain generic type alias defined in lib.es5.d.ts. Partial<T> is four lines of code. There is no compiler magic — you could write all of them yourself, and later on this page you will write one.
That matters for a practical reason: since they are ordinary generics, they compose. Partial<Omit<User, 'id'>> is a perfectly normal type, and so is Awaited<ReturnType<typeof fetchUser>>. You are not picking one utility off a menu — you are chaining small transforms, the same way you would chain array methods at runtime.
It also means they cost nothing. All of this is erased before your code runs. A twelve-layer type expression compiles to exactly the same JavaScript as no type at all, so the only budget you are spending is the compiler's and your reader's.
Object-shape utilities
These take an object type and give you back a different object type. They are the ones you will reach for daily.
Partial and Required
Partial<T> makes every property optional. Required<T> does the opposite.
type User = {
id: string
name: string
email: string
}
// Every property becomes optional
type UserPatch = Partial<User>
const patch: UserPatch = { name: 'Ada' }
// And back again — every property becomes mandatory
type StrictUser = Required<UserPatch>Partial is the type for a PATCH endpoint body, a form draft, or an options bag with defaults. Required is rarer, but it is useful right after you have merged user options into defaults and want to tell the compiler nothing is missing anymore.
Readonly
Readonly<T> marks every property as read only, so assignment is a compile error.
type AppConfig = {
apiUrl: string
retries: number
}
const config: Readonly<AppConfig> = { apiUrl: '/api', retries: 3 }
// config.retries = 5
// ❌ Cannot assign to 'retries' because it is a read-only propertyGood for config objects and anything you pass into a function that has no business mutating it. It is also the cheapest way to document intent at a call site: a parameter typed Readonly<AppConfig> tells every future caller that this function reads and does not write, and the compiler holds that promise for you.
Pick and Omit
Pick<T, K> keeps the keys you name. Omit<T, K> drops them.
type Account = {
id: string
email: string
passwordHash: string
lastLogin: Date
}
type PublicAccount = Pick<Account, 'id' | 'email'>
type SafeAccount = Omit<Account, 'passwordHash'>
const shown: PublicAccount = { id: 'a1', email: 'ada@example.com' }The rule of thumb: use Pick when the allow-list is short, Omit when the deny-list is short. SafeAccount above says "everything except the secret", which stays correct when someone adds a new field to Account. A Pick would silently leave the new field out.
Record
Record<K, V> builds an object type from a key type and a value type.
type Role = 'admin' | 'editor' | 'viewer'
const permissions: Record<Role, string[]> = {
admin: ['read', 'write', 'delete'],
editor: ['read', 'write'],
viewer: ['read'],
}The payoff is exhaustiveness. Add 'owner' to Role and the object above becomes a compile error until you fill it in — which is exactly what an index signature like { [key: string]: string[] } would not do for you. The Record page goes deeper on that trade-off.
Union-filtering utilities
These operate on union types rather than object shapes. They distribute over the union and keep or drop members.
type Status = 'draft' | 'published' | 'archived' | 'deleted'
type VisibleStatus = Exclude<Status, 'deleted' | 'archived'>
// 'draft' | 'published'
type TerminalStatus = Extract<Status, 'archived' | 'deleted' | 'banned'>
// 'archived' | 'deleted'
type MaybeId = string | null | undefined
type Id = NonNullable<MaybeId>
// stringExclude removes members, Extract keeps the ones that match, and NonNullable strips null and undefined. Exclude and Extract are distributive conditional types — they apply to the union one member at a time, which is why they work on a four-member union and a forty-member one identically. NonNullable reaches the same result by a different route: since TypeScript 4.8 it is defined as T & {}, an intersection rather than a conditional. {} accepts anything except null and undefined, so intersecting with it drops exactly those two members. Note that Extract quietly ignores 'banned' because it was never in Status — no error, it simply matches nothing.
Function and async utilities
These pull types back out of values you have already written, so you never restate a shape by hand.
function createSession(userId: string, ttlMinutes: number) {
return { userId, expiresAt: new Date(Date.now() + ttlMinutes * 60000) }
}
type Session = ReturnType<typeof createSession>
// { userId: string; expiresAt: Date }
type SessionArgs = Parameters<typeof createSession>
// [userId: string, ttlMinutes: number]
async function loadProfile() {
return { handle: 'ada' }
}
type Profile = Awaited<ReturnType<typeof loadProfile>>
// { handle: string }Awaited<T> unwraps a promise, and it recurses, so a Promise<Promise<string>> still resolves to string. Pairing it with ReturnType and typeof is the standard way to type the result of an async function without exporting a separate interface for it. See function types for the call-signature background these rely on.
The string utilities people forget
Four of them work on string literal types rather than objects or unions: Uppercase, Lowercase, Capitalize and Uncapitalize.
type EventName = 'click' | 'focus'
type HandlerName = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus'
type EnvKey = Uppercase<'api_url'>
// 'API_URL'On their own they look like a party trick. Combined with template literal types they are how libraries derive onChange from change, or a SCREAMING_CASE env key from a camelCase config field — without anyone maintaining a second list by hand.
Building one yourself
Under the hood these are mapped types over keyof. Here is Pick, rewritten from scratch:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type Article = {
slug: string
title: string
body: string
}
type ArticleCard = MyPick<Article, 'slug' | 'title'>
// { slug: string; title: string }That is the whole implementation. [P in K] loops over the keys you asked for, and T[P] looks up the original value type for each one. The K extends keyof T constraint is what makes MyPick<Article, 'nope'> an error.
Once you can write that, the rest follow the same template — Partial adds ?, Readonly adds the readonly modifier, and key remapping with template literal types lets you rename keys as you go. The union-filtering ones are not mapped types at all — Exclude and Extract are conditional types, NonNullable is an intersection — but the idea is the same: one small expression, applied across every member.
Writing your own is also the fix when a built-in almost fits. There is no RequireOnly<T, K> or DeepReadonly<T> in the standard library, and every large codebase ends up with a handful of these in a types.ts file. Better to write four lines you understand than to reach for any at the one spot where the built-ins stop. The Get Return Type challenge is the natural next step: it rebuilds ReturnType with infer.
Three gotchas
Omit does not check its keys. Pick constrains the second parameter to keyof T, but Omit does not. A typo passes silently.
type Product = {
sku: string
price: number
}
// No error, even though 'prcie' is not a key of Product
type Cheaper = Omit<Product, 'prcie'>
// Pick is strict about it
// type Broken = Pick<Product, 'prcie'>
// ❌ 'prcie' does not satisfy the constraint 'keyof Product'Cheaper is just Product again, and nothing tells you. If a field you expected to be gone keeps showing up, check the spelling first.
Partial is shallow. It only touches the top level.
type Settings = {
theme: string
editor: {
fontSize: number
wordWrap: boolean
}
}
const draft: Partial<Settings> = {
// 'editor' itself is optional, but if you supply it,
// both inner keys are still mandatory
editor: { fontSize: 14, wordWrap: true },
}If you want it to go all the way down, you write that yourself:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}Readonly is shallow too, for the same reason, and it is a compile-time marker only. Nothing stops a plain JavaScript caller from mutating the object at runtime — use Object.freeze if you need that.
Which one do I reach for
| You want to... | Use |
|---|---|
| Make all fields optional | Partial<T> |
| Make all fields mandatory | Required<T> |
| Block mutation | Readonly<T> |
| Keep a few named fields | Pick<T, K> |
| Drop a few named fields | Omit<T, K> |
| Map every key of a union to a value | Record<K, V> |
| Remove members from a union | Exclude<T, U> |
| Keep matching members of a union | Extract<T, U> |
Strip null and undefined | NonNullable<T> |
| Get a function's result type | ReturnType<F> |
| Get a function's argument tuple | Parameters<F> |
| Unwrap a promise | Awaited<T> |
| Change a string literal's casing | Uppercase<S> |
| Upper-case the first letter only | Capitalize<S> |
When not to reach for one
Derived types are not free for the person reading them. Partial<Omit<Pick<User, 'id' | 'email' | 'role'>, 'role'>> is technically correct and nobody will thank you for it. If a chain runs past two layers, give the result a name with a type alias and let the name carry the meaning.
The other limit is intent. Omit<Account, 'passwordHash'> says something true about the shape but nothing about why — a reader six months later cannot tell whether that field is secret, deprecated, or just unused here. When the why matters, a named type with a comment beats a clever one-liner. Utility types are good at keeping shapes in sync, not at documenting decisions. Intersection types are often the clearer tool when you are composing a shape up from parts rather than carving one down.
Wrap up
The value of utility types is not that they save keystrokes. It is that a derived type stays in sync with its source. Rename a field on Account and every Pick, Omit and ReturnType downstream updates on its own, or breaks loudly where a human has to look.
Start with Partial, Pick, Omit and Record — those four cover most of what a codebase needs. When you hit the ceiling, write your own with a mapped type. They are less mysterious than they look.
Become a TypeScript Pro
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges