Utility Types in TypeScript You Should Actually Use

I tour the TypeScript utility types worth using daily — Partial, Pick, Omit, Record, ReturnType and friends — with the real shapes they replace and where they stop helping.

Utility Types in TypeScript You Should Actually Use

Every TypeScript codebase has a folder of interfaces that started life as copies of each other. User. Then UpdateUserInput, which is User but everything optional. Then UserListItem, which is User minus three fields. Then someone adds an avatarUrl to User — and three of the four copies silently don’t have it. Nothing errors. The types just quietly disagree with the data.

The standard library ships the fix: a set of built-in utility types that compute those derived shapes from the source, so there’s exactly one description of User and everything else is arithmetic on it. They’re all generics wearing name tags — the machinery from part 3 — but you never have to write the machinery, only read it. Here are the ones that earn their keep daily, what they replace, and the point at which they stop helping.

Series: TypeScript, part 4 of 4 — the finale. Start: TypeScript for JavaScript Developers · Previous: Generics in TypeScript Without the Confusion.

On this page

Why hand-copied interfaces drift

The root disease is describing the same data twice. Interfaces and type aliases give you a way to name a shape — but a name is a snapshot. When the underlying thing changes, someone must remember to update every snapshot by hand, and the compiler can’t help, because each copy is internally consistent.

Utility types fix the update problem, not the naming problem: they express derived shapes as functions of the source type. Change the source, and every derived type changes with it — the drift becomes impossible rather than merely forbidden.

One interface to rule them all, for the running examples:

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "editor" | "viewer";
}

Partial and Required: flipping optionality

Partial constructs a type with all properties set to optional — the exact shape of a “changes only” payload. This is the PATCH-request type, and the docs’ own updateTodo example is the pattern:

function updateUser(id: number, changes: Partial<User>) {
  // every field optional — exactly what a partial update sends
}

updateUser(7, { name: "ada" }); // fine: nothing else required

Compare that to the hand-rolled alternative: a five-field interface where someone eventually forgets to add ? to one property, and every caller pays for it. Required is the mirror — it sets every property to required, useful when a config type is optional on input but your internals need all of it filled in.

Pick and Omit: carving views from one source

Most “subset” interfaces are really views of one thing, and TypeScript has knives for exactly that. Pick constructs a type by picking the named properties from the source; Omit keeps everything except the named properties — the docs call it “the opposite of Pick”:

type UserListItem = Pick<User, "id" | "name">;
type CreateUserPayload = Omit<User, "id">;

UserListItem is the list view — two fields, because that’s all the table shows. CreateUserPayload is everything the create form sends, minus the id the server assigns. Both are derived: add a avatarUrl to User and CreateUserPayload automatically expects it, your editor flags every form that’s now missing it, and UserListItem stays untouched because it never mentioned it.

That last sentence is the pitch. Not fewer types — fewer stale types. The union-of-keys syntax in Pick is the same keyof idea from the generics post, applied to carving instead of constraint.

Record: keyed collections that check their keys

The most common weakly-typed structure in JavaScript is the plain object used as a lookup map. Record gives it a spine: it constructs an object type whose keys are Keys and whose values are Type. Combined with an indexed access type, it does something quietly wonderful:

const roleLabels: Record<User["role"], string> = {
  admin: "Administrator",
  editor: "Editor",
  viewer: "Viewer",
};

Record<User["role"], string> reads: every possible role, as a key, with a string value. Add "owner" to the role union and this object fails to compile until the label exists. Misspell a key and it fails. Forget one and it fails. The exhaustive-lookup bug class — label missing at runtime, page shows undefined — moves to compile time where it costs nothing.

Deriving from what exists: ReturnType and friends

The last family derives types from functions and values rather than shaping them by hand. ReturnType gives you the return type of a function — point it at the source of truth and never re-describe the result:

function defaultUser() {
  return { id: 0, name: "guest" };
}

type DefaultUser = ReturnType<typeof defaultUser>; // { id: number; name: string }

Change what defaultUser returns and DefaultUser follows — no visit to a mirror-image interface required. Two neighbours earn frequent mention: Awaited, which unwraps a promise’s resolved type (so Awaited<ReturnType<typeof fetchUser>> is the user itself — the same typing that powers the async chains from the event-loop post), and NonNullable, which strips null and undefined from a union — a clean way to declare “callers must handle the empty case before they call me”:

type FromConfig = string | null;

function greet(name: NonNullable<FromConfig>) {
  console.log(`hi ${name}`);
}

Where utility types stop helping

Honesty section, because this tool is easy to overuse.

Deep nesting stops reading. Partial<Pick<Omit<User, "email">, "id">> is technically one line and practically a puzzle. The rule of thumb I’d defend: one utility layer reads fine, two needs a comment, three deserves a named type alias — or a plain hand-written interface, which was the right answer all along.

Derived types leak everywhere. If every function signature in a module ends in Partial<...> of something two imports away, readers spend their time resolving references. A named alias (type UserPatch = Partial<User>) costs one line and gives every downstream reader a word to hold onto.

Some shapes aren’t derived. When a form payload differs from the entity semantically — different field meanings, not fewer fields — forcing it through Omit obscures that. Two unrelated shapes sometimes deserve two honest declarations.

You’re reaching for The utility
“Changes only” update payloads Partial
Everything filled before use Required
A view with fewer fields Pick
Everything except a field or two Omit
A lookup keyed by known values Record
The result of an existing function ReturnType (with Awaited for promises)
“Handle null before calling me” NonNullable

Stop re-describing your data

The through-line of this TypeScript series, landing here: let the compiler hold the relationships. Part 1 turned on the checker; part 2 named your shapes; part 3 taught them to vary; this part stopped re-describing them entirely. One User, and everything else — patch payloads, list views, role maps, defaults — computed from it, and refactored with it forever.

Your move is small: find one copied interface in your current project and replace it with a utility type this week. Feel the update propagate. That feeling is the whole series in miniature — and with the language tour complete, this blog’s cadence turns next to the other half of the craft: the tools and workflows that carry typed code from your editor into the world, starting with Git.

Sources