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.
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];One source of truth, Invoice, and five views onto it. Adding a column extends every shape that includes it. 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.
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.
Field-modifier transforms
Section titled “Field-modifier transforms”These three utilities reshape every field of a type the same way.
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>;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.
Required<T>: every field required
Section titled “Required<T>: every field required”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.
Readonly<T>: every field readonly
Section titled “Readonly<T>: every field readonly”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.
Field-selection transforms
Section titled “Field-selection transforms”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.
type InvoiceInsert = Omit<Invoice, 'id' | 'createdAt' | 'updatedAt'>;The insert payload. The database generates id, createdAt, and updatedAt, so the caller never passes them. Omit drops the named keys and keeps the rest. It’s the most-reached utility in CRUD code: every INSERT payload type descends from it.
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.)
Nullability and union-set transforms
Section titled “Nullability and union-set transforms”These utilities operate on union members rather than fields. NonNullable drops null and undefined; Extract and Exclude keep or drop members by assignability.
NonNullable<T>: drop null and undefined
Section titled “NonNullable<T>: drop null and undefined”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 ofTassignable toU.Exclude<T, U>drops the members ofTassignable 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 named; TerminalStatus is 'paid' | 'void', every member except those two.
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.
Function-shape and async transforms
Section titled “Function-shape and async transforms”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.
Parameters<F>: the parameter tuple
Section titled “Parameters<F>: the parameter tuple”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.
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 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>>;Composing utility types
Section titled “Composing utility types”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.
type EditableInvoiceFields = Pick<Invoice, 'status' | 'total'>;type EditableInvoiceView = Readonly<Partial<EditableInvoiceFields>>;The named EditableInvoiceFields gives the reader a landing point, and the second line stays two utilities deep.
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.
The full reference table
Section titled “The full reference table”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.
| 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 |
What this lesson doesn’t reach for
Section titled “What this lesson doesn’t reach for”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.
Exercise: pick the right utility
Section titled “Exercise: pick the right utility”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.
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>Matching all eight on sight means the recall is there. Only item 1 (Partial<Omit<...>>) composes two utilities; the rest map to one each.
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.