Overview

Utility types are the built-in helpers that derive one type from another. Use them instead of rewriting the same shape by hand; when they fall short, write a small mapped or conditional type. Read typescript-generics first; every utility below is a generic over an existing type.

Pick and Omit to subset object types

Pick<T, K> keeps the named keys; Omit<T, K> drops them. Use these when a function takes a subset of an existing shape.

type User = {
  id: string
  email: string
  passwordHash: string
  createdAt: Date
}
 
type PublicUser = Omit<User, "passwordHash">
type LoginInput = Pick<User, "email"> & { password: string }

When a function returns “the input minus one field,” Omit keeps the two types in lockstep. Rename the field and both update. See typescript-types-vs-interfaces for when & is the wrong combiner.

Partial for patches; Required to lock fields in

Partial<T> makes every field optional. Use it for PATCH-style update payloads.

type User = { id: string; name: string; bio: string }
 
async function updateUser(id: string, patch: Partial<Omit<User, "id">>) {
  // ...
}

Required<T> does the inverse: makes every field non-optional. Use it after validation to assert that a previously-loose input is now fully populated.

type Draft = { name?: string; price?: number }
type Final = Required<Draft>  // { name: string; price: number }

Partial is the most overused utility; do not reach for it as a shortcut around modeling. A type with every field optional is a type that says nothing.

Readonly for immutable shapes

Readonly<T> marks every field readonly. Use it for config, constants, and any value passed by reference that should not be mutated.

const CONFIG: Readonly<{ apiBase: string; retries: number }> = {
  apiBase: "https://api.example.com",
  retries: 3,
}
 
// CONFIG.retries = 5  // Error

readonly is shallow. For deep immutability, write a DeepReadonly<T> helper or, more often, freeze the value with Object.freeze and trust the type system at the top level.

Record for typed key-value maps

Record<K, V> types an object whose keys are K and values are V. Use it for lookup tables and switch-replacement maps.

type Status = "draft" | "sent" | "paid"
 
const LABELS: Record<Status, string> = {
  draft: "Draft",
  sent: "Sent",
  paid: "Paid",
}

The compiler errors if you add a new status to the union and forget to add a label. That is the win; without Record, the map drifts from the union silently. Pair with as const (see typescript) for literal preservation.

Exclude, Extract, and NonNullable for unions

These three work on union types, not object types. Confusing them with Pick/Omit is the most common utility-type mistake.

type Status = "draft" | "sent" | "paid" | "void"
 
type Live = Exclude<Status, "void">           // "draft" | "sent" | "paid"
type Closed = Extract<Status, "paid" | "void">// "paid" | "void"
 
type MaybeUser = User | null | undefined
type DefiniteUser = NonNullable<MaybeUser>    // User

NonNullable<T> is Exclude<T, null | undefined>. Reach for it after a check, when you want to express “I have already proven this is set.” See typescript-narrowing for the runtime side of the same idea.

ReturnType and Parameters for function inference

ReturnType<F> is the return type of F; Parameters<F> is its tuple of arguments. Use these when a function’s signature is the source of truth and you do not want to restate it.

function loadUser(id: string) {
  return { id, name: "Ada" } as const
}
 
type LoadedUser = ReturnType<typeof loadUser>      // { id: string; name: "Ada" }
type LoadArgs = Parameters<typeof loadUser>        // [id: string]

This pattern is the right tool for “the return type matters but spelling it out duplicates the function body.” It is the wrong tool for designing an API; design the type first, then write the function to fit.

Awaited to unwrap promises

Awaited<T> resolves nested promises to the eventual value. The compiler uses it internally for await; use it explicitly when the source is a Promise<Promise<T>> or a ReturnType of an async function.

async function loadInvoice(id: string) {
  return { id, amount: 100 }
}
 
type Invoice = Awaited<ReturnType<typeof loadInvoice>>  // { id: string; amount: number }

Before Awaited, the idiom was a hand-rolled conditional with infer. The built-in handles the recursive unwrap correctly; use it.

Roll your own when the built-ins fall short

The built-ins cover 80% of cases. For the rest, a small mapped or conditional type is the answer. Three patterns worth knowing:

// Strict pick: error if a key in K is not in T (Pick allows extras silently).
type StrictPick<T, K extends keyof T> = { [P in K]: T[P] }
 
// Mutable: drop readonly from every field.
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
 
// PartialBy: make a specific subset of keys optional, keep the rest required.
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

Name the helper, place it in a types.ts module, and reuse it. Avoid pasting the same five-line type expression into ten files; the named alias survives refactors better. See typescript-generics for the conditional-type and infer rules.