Tags: web-dev concept

Utility Types

Date: 2026-09-28


The built-in utility types aren’t magic — each is a two-line mapped or conditional type you could write yourself. Learn the two building blocks and the dozen utilities stop being a list to memorise and become a pattern you can read, adapt and extend.


Utility types are generic type aliases shipped with TypeScript’s standard library that transform one type into another — making properties optional, picking a subset, stripping null from a union. Every one is defined in lib.es5.d.ts or a sibling, in ordinary TypeScript. They’re Generics applied to types rather than functions.

The two building blocks

Mapped types loop over keys and rebuild an object type:

type Partial<T>  = { [K in keyof T]?: T[K] }
//                   └──┬───────┘ │  └─┬┘
//                   for each key │  keep the value type
//                                └ add optional

Conditional types branch on assignability, and distribute over unions — applied to each member separately, with never results vanishing:

type Exclude<T, U> = T extends U ? never : T
 
Exclude<'gbp' | 'eur' | 'usd', 'usd'>
  = ('gbp' extends 'usd' ? never : 'gbp')     → 'gbp'
  | ('eur' extends 'usd' ? never : 'eur')     → 'eur'
  | ('usd' extends 'usd' ? never : 'usd')     → never   ← drops out
  = 'gbp' | 'eur'

infer inside a conditional captures part of a matched type — which is how ReturnType extracts a return type from a function type.

The ones worth knowing, with their definitions

UTILITY              WHAT IT DOES                     DEFINED AS (simplified)
───────────────────  ───────────────────────────────  ──────────────────────────────────────────
Partial<T>           every property optional          { [K in keyof T]?: T[K] }
Required<T>          every property required          { [K in keyof T]-?: T[K] }
Readonly<T>          every property readonly          { readonly [K in keyof T]: T[K] }
Pick<T, K>           keep these keys                  { [P in K]: T[P] }
Omit<T, K>           drop these keys                  Pick<T, Exclude<keyof T, K>>
Record<K, V>         object with keys K, values V     { [P in K]: V }
Exclude<U, X>        union minus members matching X   U extends X ? never : U
Extract<U, X>        union members matching X         U extends X ? U : never
NonNullable<T>       remove null and undefined        T & {}
ReturnType<F>        a function's return type         F extends (...a: any) => infer R ? R : any
Parameters<F>        a function's parameter tuple     F extends (...a: infer P) => any ? P : never
Awaited<T>           unwrap Promise, recursively      (recursive conditional; unwraps thenables)
NoInfer<T>           block inference at this site     (compiler intrinsic, 5.4 — see Generics)

The -? in Required removes the optional modifier; +/- work on readonly too.

In practice

The patterns that recur in commerce code:

  • Update payloads — Partial<Pick<Customer, 'email' | 'marketingOptIn'>>: a PATCH body may send any subset of the editable fields, and only those
  • Lookup tables — Record<Currency, { symbol: string; decimals: number }> fails to compile when a currency is added to the union and not to the table, which is exhaustiveness for data rather than code
  • Deriving from a function you don’t own — Awaited<ReturnType<typeof fetchOrder>> gives the resolved type without redeclaring it, so it tracks the library
  • Stripping internals — Omit<Order, 'internalNotes' | 'costPrice'> for what a storefront API returns

Where they mislead

  • Omit doesn’t check its keys. K extends keyof any — any string is accepted, so Omit<Order, 'totl'> compiles and omits nothing. Rename a field and every Omit of the old name silently stops working. Pick does check (K extends keyof T)
  • Omit on a union flattens it. keyof (A | B) is only the keys they share, so Omit<Payment, 'id'> loses every member-specific property. A distributive version — T extends unknown ? Omit<T, K> : never — keeps the union
  • Partial is shallow. Nested objects stay fully required. A deep version is a recursive mapped type, and a common source of unreadable errors
  • Readonly is compile-time only and shallow; Object.freeze is the runtime equivalent, also shallow
  • Record<string, T> claims every key exists. Reading prices['xyz'] gives T, not T | undefined, unless noUncheckedIndexedAccess is on — Type Safety Boundaries

When a type needs three nested utilities, name the intermediate steps as their own aliases. Same rule as code: a named intermediate is documentation.