Skip to content
Chapter 5Lesson 6

The utility-type toolbox

A tour of TypeScript's built-in 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 installed the operators that lift values into types: typeof, keyof, and T[K]. This lesson sits one layer above them. TypeScript ships a small set of utility types , generic type aliases like Partial<T> and Pick<T, K> that take a type and return a transformed version of it. When you need a variation on a shape you already have, name the utility instead of restating the shape by hand.

Picture a single feature in the SaaS code you’ll write. You have an Invoice type exported from the database schema, and for this one feature you need five different 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 type of an existing saveInvoice action so a wrapper can forward it. You could hand-write every one of those shapes, or you could derive them from Invoice with the utilities below.

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;
};

This is five sources of truth, all running parallel to Invoice. Adding a column to Invoice means hand-editing every shape that should include it and remembering which ones shouldn’t. Renaming total to amountCents updates Invoice and leaves the three shapes that still say total out of date. Neither reviewers nor the build flag the mismatch, because nothing connects these copies back to Invoice.

That contrast is the whole motivation. 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 composition rule that keeps chains legible.

One scope note before the tour: this lesson uses a hand-rolled Invoice type to keep the focus on the slicing. In real code, the source type comes from Drizzle’s $inferSelect (the row type derived from the schema) or Zod’s z.infer (the validated type derived from the schema), both of which ship in later units. Either one produces the canonical Invoice shape, and the utilities below slice it the same way no matter where it came from.

Here is the running shape for the lesson, with the same brand and discriminated-union vocabulary the chapter has been using:

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 Invoice['status'] line uses indexed access from the previous lesson: it pulls the status union off the type rather than restating it. You’ll see this pattern reused several times below.

The first group reshapes every field of a type the same way. There are three of them, and all three are shallow: they touch only the top level of the shape.

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>;

What does Partial<Invoice> actually evaluate to? Hover the utility name to see:

type InvoiceUpdate = Partial<Invoice>;

Every field gains a ?, and that is the whole utility. A PATCH /invoices/:id body parser accepts a Partial<Invoice>, and the handler updates only the fields the caller sent. Fields that were already nullable, like notes: string | null, end up as notes?: string | null. The ? and the | null express different things: the property can be absent, or the value can be null. Both carry through cleanly.

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

Reach for it when you need the post-defaults shape. A config type declares its fields as optional at the write site so callers can omit them and pick up defaults. By the time the rest of the app reads the config, the defaults have been applied and every field is present. 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' }, with no question marks. The function that fills in defaults returns AppConfig, so every consumer past that point reads non-optional fields without a narrowing check.

Reach for this when you have a return value you don’t want the caller mutating. It marks every field readonly at the type level, 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 the value-level as const from the previous chapter’s lesson “Keeping literals narrow with as const and satisfies”. The two work at different sites. as const freezes a value: every literal narrows, and every property becomes readonly recursively. Readonly<T> transforms a type: every top-level property becomes readonly, and nothing recurses. Reach for as const when authoring the value, and for Readonly<T> when describing a value’s read-only view at a type boundary.

The second group selects a subset of fields by name. There are two utilities, and they are two sides of the same operation.

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

This is the slim list-view row. The list endpoint doesn’t need notes, timestamps, or the org pointer, only the four fields the table actually renders. Pick keeps the named keys and drops the rest.

Both utilities take a key union as their second argument: 'id' | 'createdAt' | 'updatedAt' is itself a type, a union of string literals. They are the two halves of the same field-selection operation, where Pick says what to keep and Omit says what to drop. Reach for whichever reads cleaner at the call site. That’s usually Pick for slim DTOs (three fields kept reads better than ten dropped), and Omit for “the same as the row, minus the DB-controlled columns” (three dropped reads better than ten kept).

One mechanical detail is worth naming. Pick<T, K> and Omit<T, K> both constrain K to keyof T. The internal type signature is Pick<T, K extends keyof T>, so if you pass a key that isn’t actually in T, the compiler refuses. Typing 'totl' instead of 'total' is a compile error at the Pick call site, not a silent runtime mismatch. (You’ll write this same K extends keyof T constraint yourself in the next lesson, when generics with constraints land.)

The third group operates on union members rather than fields. NonNullable removes null and undefined from a union, while Extract and Exclude slice union members by assignability.

Reach for this when you’ve narrowed a value and need its non-null type to feed into a slot that demands one.

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

User['avatarUrl'] is string | null, read with indexed access from the last lesson. Wrapping it in NonNullable<...> strips the null and leaves string. You usually reach for this after an if (user.avatarUrl !== null) narrow: the value is non-null inside the block, 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”

These are two halves of the same cut. Both take a union type T and split it against another type U.

  • Extract<T, U> keeps the members of T that are assignable to U.
  • Exclude<T, U> drops the members of T that are 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 the second argument names. TerminalStatus is 'paid' | 'void', every member except those two. It’s the same cut, expressed two ways.

The same operation, visualized:

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.

Reach for Extract when the subset you want is small and easier to name positively (“the two statuses where editing is allowed”). Reach for Exclude when the complement is small and easier to name by what you don’t want (“everything that isn’t a terminal state”). Either form produces the same type when the cut is the same, so pick the one that reads cleaner.

One thing to watch for: Extract and Exclude care about assignability, not literal equality. Exclude<string | number, number> is string, because the rule is structural rather than by-name. For literal-union narrowing, which is the common case, this distinction rarely surprises you; for broader types it can. If you ever see Exclude quietly returning never, check whether the type you’re excluding is assignable from every member of the input.

The fourth group reads function and Promise shapes. In SaaS code, the function whose shape you want is almost always already written: an existing Server Action, fetcher, or helper. Restating its parameters and return type by hand creates a second source of truth that drifts the moment the function changes signature, 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 one inside-out. typeof saveInvoice lifts the function value into the type register (the previous lesson’s bridge), and ReturnType<...> then reads the function type’s return position. The result is Promise<Invoice>. Any downstream code that consumes saveInvoice’s result types its variable as SaveResult and stays in sync with the function automatically.

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

Parameters<...> returns the function’s parameters as a tuple type, not a single value. For a single-argument function, the parameter sits at position [0], and the indexed access (also from the previous lesson) pulls it out. SaveArgs is the { id; status; total } shape saveInvoice accepts, read off the function itself instead of restated. Reach for this when you’re typing a wrapper or higher-order function that forwards arguments to saveInvoice without rewriting the parameter shape.

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 the resolved value as Invoice[]. The utility also handles recursive Promises: Awaited<Promise<Promise<User>>> is User, not Promise<User>. That covers the case where a function returns a Promise that resolves to another Promise, which is rare, but the utility handles it without making you think about it.

Step through the chain on hover:

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

That two-utility chain leads straight into the next section.

Utility types compose. Each one is a generic type alias whose input is a type and whose output is a type, so the output of one is a valid input for another. The guideline is to chain two utilities when the chain is more legible than the alternative. Past two, the chain becomes harder to read than a named intermediate alias, and the cost flips.

Two canonical chains carry their weight 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. For the first chain, you take Invoice, drop the DB-controlled fields, then mark the rest optional. For the second, you take fetchInvoices as a value, lift it to its type, read the return, and unwrap the Promise. Train yourself to read chains inside-out, since you’ll read more chains than you write.

The rule about chain depth is short: two utilities is the comfortable ceiling, and at three you should name an intermediate alias. A reader has to mentally evaluate every layer of the chain to know what the final type is, and three layers of nested generics cost more than a name does.

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

Three utilities are nested here. To know what EditableInvoiceView is, the reader evaluates inside-out: Pick keeps status and total, Partial marks both optional, and Readonly marks both readonly. The result is correct, but the path to it lives inside the generics, so every reviewer pays the cost again.

The rule isn’t strict. Partial<Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>> is two utilities, reads fine, and shows up in every CRUD codebase. The line moves the moment a third utility joins. Name the intermediate, keep the depth at two, and the next person to read the file has an easier time.

Here is a reference table you can come back to three months from now when you’ve forgotten which utility produces which shape. The order matches the lesson’s groups: field-modifier first, then field-selection, construction, nullability, union-set, function-shape, and async.

Record<K, V> carries over from the previous chapter’s lesson “Dynamic keys — index signatures and Record”. It’s the construction utility for lookup maps keyed by a literal union, included here for completeness even though it sits outside the “eleven” framing.

| Utility | What it returns | Reach for it when… | | --- | --- | --- | | Partial<T> | Every field optional | PATCH-style partial updates | | Required<T> | Every field required | Post-defaults config read shape | | Readonly<T> | Every field readonly | Returned value the caller shouldn’t mutate | | Pick<T, K> | Only the named fields | Slim list-view or detail DTO | | Omit<T, K> | All fields except the named | Insert payload, DB-controlled fields removed | | Record<K, V> | Object with K keys, V values | Lookup map keyed by a literal union | | NonNullable<T> | T without null or undefined | Post-narrow slot demanding non-null | | Extract<T, U> | Members of T assignable to U | Subset of a lifecycle union (e.g. editable states) | | Exclude<T, U> | Members of T not assignable to U | Complement of an Extract, e.g. terminal states | | ReturnType<F> | Return type of function F | Consuming an existing function’s output shape | | Parameters<F> | Parameters of F as a tuple | Typing a wrapper around an existing function | | Awaited<T> | Resolved type of Promise<T> (recursive) | Async function’s eventually-yielded value |

That’s the daily-reach surface, and it’s worth bookmarking to come back to.

A few utility types aren’t on the daily reach. They’re named here so you recognize them when they appear in library types and know which side of the “use this / look it up” line they sit on.

Capitalize, Uncapitalize, Uppercase, and Lowercase are string-literal transforms: they take a string-literal type and return another. You’ll see them in template-literal type patterns when Next.js typed routes land later in the course. They’re rare in app-level type aliases.

InstanceType<C> extracts the instance type of a class constructor. Classes get one narrow lesson later in this unit, and InstanceType follows them there. You’ll rarely reach for it outside that context.

NoInfer<T> was added in TypeScript 5.4. You need it for a generic wrapper whose default parameter would otherwise widen the inferred type. You’ll hit this case in the next lesson when generics with constraints land, and the fix is NoInfer<T> on the offending parameter position. It’s named once here so you know the tool exists when the situation arises.

Mapped types ({ [K in keyof T]: ... }) are the underlying mechanism every utility above is built from. Authoring them yourself is library-author territory: reach for them only when the codebase needs the same custom transform three or more times and a chain of built-ins won’t express it. The lesson stops at the built-ins because that’s the daily reach.

infer and conditional types are both out of scope. They’re the next layer down, the one library authors live in. App code reaches for the built-ins above and lets the library handle the rest.

There are eight “I want this shape” prompts on the left and the utility-type expression that produces each one on the right. The skill this lesson builds is reaching for the right name from the shape you want, and this exercise checks that it’s there.

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>

If you got all eight without rereading the table, you can match a shape to its utility on sight, which is exactly what the daily work asks of you. The composition pair (item 1, Partial<Omit<...>>) is the only one that requires the inside-out read, since every other prompt maps to a single utility.