Skip to content
Chapter 5Lesson 6

TypeScript's built-in utility types

The built-in TypeScript utility types, Partial, Pick, Omit, ReturnType, and the rest, that derive new shapes from one source type instead of restating them by hand.

The last lesson added the operators that turn values into types: typeof, keyof, and T[K]. This lesson sits one layer up. TypeScript ships a small set of utility types : generic aliases like Partial<T> and Pick<T, K>. When you need a variation on a shape you already have, name the utility instead of restating the shape by hand.

Picture one feature in a web app. You have an Invoice type from the database schema, and this feature needs five shapes of it: a partial-update payload for PATCH, an insert payload with the DB-controlled fields removed, the subset of statuses that still allow edits, the resolved value of an async fetcher, and the first argument of an existing saveInvoice action so a wrapper can forward it. You can hand-write all five, or derive them from Invoice.

type InvoiceUpdate = {
orgId?: OrgId;
status?: InvoiceStatus;
total?: number;
currency?: string;
notes?: string | null;
};
type InvoiceInsert = {
orgId: OrgId;
status: InvoiceStatus;
total: number;
currency: string;
notes: string | null;
};
type EditableStatus = 'draft' | 'sent';
type InvoicesResult = Invoice[];
type SaveInvoiceArgs = {
id: InvoiceId;
status: InvoiceStatus;
total: number;
};

Five sources of truth, all running parallel to Invoice. Adding a column means editing every shape that should include it and remembering which shouldn’t. Renaming total to amountCents updates Invoice and leaves the three shapes that still say total wrong. Nothing connects these copies back to Invoice, so neither reviewers nor the build catch the mismatch.

The rest of the lesson names the eleven utility types you’ll reach for in 2026 SaaS code, groups them by what they reshape, and shows the rule that keeps composed chains legible.

One note before the tour: this lesson hand-rolls Invoice to keep the focus on slicing. In real code the source type comes from Drizzle’s $inferSelect or Zod’s z.infer, both in later units; the utilities below slice it the same way wherever it came from.

Here is the running shape, using the brand and discriminated-union vocabulary from earlier in the chapter:

type Invoice = {
id: InvoiceId;
orgId: OrgId;
status: 'draft' | 'sent' | 'paid' | 'void';
total: number;
currency: string;
notes: string | null;
createdAt: Date;
updatedAt: Date;
};
type InvoiceStatus = Invoice['status'];

The InvoiceStatus line uses indexed access from the last lesson to pull the status union off Invoice rather than restating it. You’ll reuse this pattern below.

These three utilities reshape every field of a type the same way.

Reach for this when you need a partial-update payload, where the caller sends only the fields they want to change.

type InvoiceUpdate = Partial<Invoice>;

Hover the utility name to see what Partial<Invoice> evaluates to:

type InvoiceUpdate = Partial<Invoice>;

Every field gains a ?. A PATCH /invoices/:id body parser accepts a Partial<Invoice>, and the handler updates only the fields the caller sent. An already-nullable field like notes: string | null becomes notes?: string | null: the ? says the property can be absent, the | null says the value can be null, and both carry through.

The mirror of Partial: it strips the ? off every optional field.

Reach for it when you need the post-defaults shape. A config type marks its fields optional at the write site so callers can omit them and pick up defaults. Once those defaults are applied, every field is present, and Required is that read-side shape.

type AppConfigInput = {
port?: number;
host?: string;
logLevel?: 'debug' | 'info' | 'warn';
};
type AppConfig = Required<AppConfigInput>;

AppConfig is { port: number; host: string; logLevel: 'debug' | 'info' | 'warn' }, no question marks. The function that fills in defaults returns AppConfig, so every consumer past it reads non-optional fields without a narrowing check.

Reach for this when you return a value the caller shouldn’t mutate. It marks every field readonly, so the compiler refuses an assignment.

type FrozenInvoice = Readonly<Invoice>;
const invoice: FrozenInvoice = getInvoice(id);
invoice.total = 0;
// ~~~~~
// Cannot assign to 'total' because it is a read-only property.

Readonly<T> is the type-level companion to value-level as const from the previous chapter’s “as const and satisfies”. as const freezes a value, narrowing every literal and marking every property readonly recursively. Readonly<T> transforms a type, marking every top-level property readonly without recursing. Use as const when authoring the value, Readonly<T> when describing a read-only view at a type boundary.

These two utilities select a subset of fields by name, as opposite sides of the same operation.

type InvoiceListItem = Pick<Invoice, 'id' | 'total' | 'currency' | 'status'>;

The slim list-view row. The list endpoint doesn’t render notes, timestamps, or the org pointer, only these four fields. Pick keeps the named keys and drops the rest.

The second argument is itself a type, a union of string literals. Pick says what to keep, Omit says what to drop, so reach for whichever reads cleaner at the call site: Pick for slim DTOs (four fields kept beats four dropped), Omit for “the row minus the DB-controlled columns” (three dropped beats five kept).

Both constrain K to keyof T, so a key that isn’t in T is a compile error: 'totl' instead of 'total' fails at the Pick call, not silently at runtime. (You’ll write this same K extends keyof T constraint yourself in the next lesson.)

These utilities operate on union members rather than fields. NonNullable drops null and undefined; Extract and Exclude keep or drop members by assignability.

Reach for this after you narrow a value and need to feed its non-null type into a slot that demands one.

type User = { id: UserId; avatarUrl: string | null };
type AvatarUrl = NonNullable<User['avatarUrl']>;

User['avatarUrl'] is string | null; wrapping it in NonNullable<...> strips the null and leaves string. The typical case: inside an if (user.avatarUrl !== null) block the value is non-null, and a downstream helper’s parameter is typed NonNullable<User['avatarUrl']>.

Extract<T, U> and Exclude<T, U>: slice a union

Section titled “Extract<T, U> and Exclude<T, U>: slice a union”

Both take a union T and split it against another type U:

  • Extract<T, U> keeps the members of T assignable to U.
  • Exclude<T, U> drops the members of T assignable to U.
type InvoiceStatus = 'draft' | 'sent' | 'paid' | 'void';
type EditableStatus = Extract<InvoiceStatus, 'draft' | 'sent'>;
type TerminalStatus = Exclude<InvoiceStatus, 'draft' | 'sent'>;

EditableStatus is 'draft' | 'sent', the two members named; TerminalStatus is 'paid' | 'void', every member except those two.

InvoiceStatus = 'draft' | 'sent' | 'paid' | 'void'
'draft'
'sent'
'paid'
'void'
Extract<…, 'draft' | 'sent'>
'draft'
'sent'
Exclude<…, 'draft' | 'sent'>
'paid'
'void'
Two ways to name the same cut — Extract keeps the green slice, Exclude keeps the orange one.

The two produce the same type from the same cut, so pick the one that reads cleaner: Extract when the subset is small and easier to name positively (“the two statuses where editing is allowed”), Exclude when the complement is small and easier to name by what you don’t want (“everything that isn’t terminal”).

One caveat: both care about assignability, not literal equality. Exclude<string | number, number> is string because the match is structural. Literal-union narrowing, the common case, rarely surprises you here, but broader types can. If Exclude quietly returns never, check whether the type you’re excluding is assignable from every member of the input.

These utilities read function and Promise shapes. The function whose shape you want is almost always already written: a Server Action, fetcher, or helper. Restating its parameters and return type by hand creates a second source of truth that drifts the moment the signature changes, so derive the shape from the function instead.

ReturnType<F>: the function’s return type

Section titled “ReturnType<F>: the function’s return type”
const saveInvoice = async (input: {
id: InvoiceId;
status: InvoiceStatus;
total: number;
}): Promise<Invoice> => {
// ...
};
type SaveResult = ReturnType<typeof saveInvoice>;

Read this inside-out. typeof saveInvoice lifts the function value into the type register, and ReturnType<...> reads its return position, giving Promise<Invoice>. Code that consumes the result types its variable as SaveResult and tracks the function automatically.

type SaveArgs = Parameters<typeof saveInvoice>[0];

Parameters<...> returns the parameters as a tuple type. The single argument sits at position [0], and indexed access pulls it out, so SaveArgs is the { id; status; total } shape saveInvoice accepts, read off the function rather than restated. Reach for this when typing a wrapper that forwards arguments to saveInvoice.

const fetchInvoices = async (): Promise<Invoice[]> => {
// ...
};
type Invoices = Awaited<ReturnType<typeof fetchInvoices>>;

Awaited<T> unwraps a Promise<T> to its resolved type: ReturnType<typeof fetchInvoices> is Promise<Invoice[]>, and Awaited<...> reads it as Invoice[]. It also unwraps recursively, so Awaited<Promise<Promise<User>>> is User.

Step through the chain on hover:

type Invoices = Awaited<ReturnType<typeof fetchInvoices>>;

Utility types compose. Each takes a type and returns a type, so one utility’s output is a valid input for the next. Two chains earn their keep in real code.

Omit<T, K> plus Partial<T> gives the slim partial-update DTO.

type InvoiceUpdate = Partial<Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>>;

ReturnType<F> plus Awaited<T> gives the resolved value of an async function.

type Invoices = Awaited<ReturnType<typeof fetchInvoices>>;

Both read inside-out: the innermost utility runs first, and each outer one transforms its output. The first takes Invoice, drops the DB-controlled fields, then marks the rest optional. The second takes fetchInvoices as a value, lifts it to its type, reads the return, and unwraps the Promise.

Two utilities is the comfortable ceiling. A reader evaluates every layer to know the final type, and at three the nested generics cost more than a name does, so split the third into a named intermediate alias.

type EditableInvoiceView = Readonly<Partial<Pick<Invoice, 'status' | 'total'>>>;

To read EditableInvoiceView, you evaluate three layers: Pick keeps status and total, Partial marks both optional, Readonly marks both readonly. The type is correct, but the path to it lives inside the generics, so every reader pays the cost again.

A two-utility chain like Partial<Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>> reads fine and appears in every CRUD codebase; the line moves only when a third utility joins.

This is the reference table for all eleven utilities, ordered by the lesson’s groups: field-modifier, field-selection, construction, nullability, union-set, function-shape, then async.

Record<K, V> carries over from the previous chapter’s “Dynamic keys” lesson and is included here for completeness.

UtilityWhat it returnsReach for it when…
Partial<T>Every field optionalPATCH-style partial updates
Required<T>Every field requiredPost-defaults config read shape
Readonly<T>Every field readonlyReturned value the caller shouldn’t mutate
Pick<T, K>Only the named fieldsSlim list-view or detail DTO
Omit<T, K>All fields except the namedInsert payload, DB-controlled fields removed
Record<K, V>Object with K keys, V valuesLookup map keyed by a literal union
NonNullable<T>T without null or undefinedPost-narrow slot demanding non-null
Extract<T, U>Members of T assignable to USubset of a lifecycle union (e.g. editable states)
Exclude<T, U>Members of T not assignable to UComplement of an Extract, e.g. terminal states
ReturnType<F>Return type of function FConsuming an existing function’s output shape
Parameters<F>Parameters of F as a tupleTyping a wrapper around an existing function
Awaited<T>Resolved type of Promise<T> (recursive)Async function’s eventually-yielded value

A few more utility types show up in library types but not in everyday app code: the string-literal transforms Capitalize, Uncapitalize, Uppercase, and Lowercase, plus InstanceType<C> (the instance type of a class constructor) and NoInfer<T> (which blocks a parameter from widening an inferred type). One layer beneath all of them sit mapped types ({ [K in keyof T]: ... }), infer, and conditional types: the mechanism every built-in is made from, and library-author territory. App code reaches for the built-ins above and lets the library handle the rest.

The skill this lesson builds is reaching for the right utility name from the shape you want, and this exercise tests that recall.

Match each shape you want to the utility-type expression that gives it to you. Click an item on the left, then its match on the right. Press Check when done.

The PATCH /invoices/:id body — partial fields, DB columns dropped
Partial<Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>>
The insert payload — Drizzle fills id and timestamps
Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>
The slim list-view row (id, total, currency, status)
Pick<Invoice, 'id' | 'total' | 'currency' | 'status'>
The resolved value of fetchInvoices
Awaited<ReturnType<typeof fetchInvoices>>
The first argument of saveInvoice
Parameters<typeof saveInvoice>[0]
The editable subset of InvoiceStatus
Extract<InvoiceStatus, 'draft' | 'sent'>
User['avatarUrl'] after narrowing out null
NonNullable<User['avatarUrl']>
The post-defaults config — every field present
Required<AppConfigInput>

Matching all eight on sight means the recall is there. Only item 1 (Partial<Omit<...>>) composes two utilities; the rest map to one each.