Overview

Utility types derive one type from another so the two stay in lockstep when the source changes. Use them instead of restating a shape by hand, and write a small mapped or conditional type when they fall short. The lookup table of every built-in is typescript-utility-types. Read typescript-generics first.

Derive subsets with Pick and Omit

Use Omit<User, "passwordHash"> for “the input minus one field” so renaming the field updates both types. Omit takes any string key, so a typo compiles and silently omits nothing; prefer Pick for a short allow-list. Neither distributes over a union: Omit<A | B, "x"> collapses to the common properties. Write a distributive version when you need one.

type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never

Use Partial for patches, not as a modeling shortcut

Partial<Omit<User, "id">> is right for a PATCH payload. A type with every field optional says nothing about which fields matter, so model required fields properly and apply Partial only to the update shape. Required<T> fits after validation, when you know a loose input is now complete.

Use Readonly shallowly and Record over unions

Readonly<T> is shallow; nested objects stay mutable. Record<Status, string> over a union key forces an entry for every member, so adding a status fails to compile until the label exists, which a plain { [key: string]: string } map would not.

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

Use Exclude, Extract, and NonNullable on unions only

They filter union members, not object keys. Mixing them up with Pick and Omit is the most common mistake. NonNullable<T> is T & {}, which fits after a check proved a value is set (typescript-narrowing).

Derive from functions with ReturnType, Parameters, and Awaited

Use them when the function is the source of truth and restating its type would duplicate the body. For an async function, nest them: Awaited<ReturnType<typeof loadInvoice>>. Do not use them to design an API; write the type first and fit the function to it.

Write a named helper instead of repeating an expression

type Mutable<T> = { -readonly [K in keyof T]: T[K] }
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

Put helpers in one types.ts module and name them, rather than pasting the same five-line type into ten files.