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.
type InvoiceUpdate = Partial<Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>>;type InvoiceInsert = Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>;type EditableStatus = Extract<InvoiceStatus, 'draft' | 'sent'>;type InvoicesResult = Awaited<ReturnType<typeof fetchInvoices>>;type SaveInvoiceArgs = Parameters<typeof saveInvoice>[0];Now there is one source of truth, Invoice, and five views onto it. Adding a column extends every shape that includes that column automatically. Renaming a column fails to compile at every consumer until you fix it. The maintenance burden moves from the team’s discipline to the compiler.
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.
Field-modifier transforms
Section titled “Field-modifier transforms”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.
Partial<T>: every field optional
Section titled “Partial<T>: every field optional”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.
Required<T>: every field required
Section titled “Required<T>: every field required”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.
Readonly<T>: every field readonly
Section titled “Readonly<T>: every field readonly”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.
Field-selection transforms
Section titled “Field-selection transforms”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.
type InvoiceInsert = Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>;This is the insert payload. The database fills id (a generated UUID), createdAt, and updatedAt, so the caller doesn’t pass them. Omit removes the named keys and keeps the rest. It’s the most-reached utility in CRUD code: every INSERT payload type you’ll write descends from it.
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.)
Nullability and union-set transforms
Section titled “Nullability and union-set transforms”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.
NonNullable<T>: drop null and undefined
Section titled “NonNullable<T>: drop null and undefined”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 ofTthat are assignable toU.Exclude<T, U>drops the members ofTthat are assignable toU.
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:
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.
Function-shape and async transforms
Section titled “Function-shape and async transforms”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.
Parameters<F>: the parameter tuple
Section titled “Parameters<F>: the parameter tuple”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.
Awaited<T>: unwrap a Promise
Section titled “Awaited<T>: unwrap a Promise”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.
Composing utility types
Section titled “Composing utility types”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.
type EditableInvoiceFields = Pick<Invoice, 'status' | 'total'>;type EditableInvoiceView = Readonly<Partial<EditableInvoiceFields>>;The intermediate EditableInvoiceFields is a named landing point. The reader sees the name, knows what it holds, and the second line stays two utilities deep. You split the expression at the point where it would otherwise get hard to follow.
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.
The eleven at a glance
Section titled “The eleven at a glance”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.
What this lesson doesn’t reach for
Section titled “What this lesson doesn’t reach for”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.
Exercise: pick the right utility
Section titled “Exercise: pick the right utility”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.
Partial<Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>>id and timestampsOmit<Invoice, 'id' | 'createdAt' | 'updatedAt'>id, total, currency, status)Pick<Invoice, 'id' | 'total' | 'currency' | 'status'>fetchInvoicesAwaited<ReturnType<typeof fetchInvoices>>saveInvoiceParameters<typeof saveInvoice>[0]InvoiceStatusExtract<InvoiceStatus, 'draft' | 'sent'>User['avatarUrl'] after narrowing out nullNonNullable<User['avatarUrl']>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.
External resources
Section titled “External resources”The canonical reference for every utility type in the language. Bookmark this — you'll come back when you need to confirm a signature.
Matt Pocock's free Total TypeScript Essentials chapter on object types — walks through Pick, Omit, Partial, and Required with the same composition-first framing this lesson uses.
The layer beneath every utility above — the mechanism `Partial`, `Required`, and the others are built from. The 'want to go further' door, with the lesson's 'don't reach for this yet' framing intact.