TypeScript Utility Types You Should Actually Know
TypeScript ships a toolbox of utility types that delete boilerplate. The ones I use every week, with the real scenarios where each one earns its place.
On this page
- Partial and Required: PATCH bodies and config
- Readonly: freeze the things that should never mutate
- Pick and Omit: API DTOs from one source of truth
- Record: typed maps and lookup tables
- Exclude and Extract: filtering unions
- ReturnType, Parameters, and Awaited: derive types from functions
- Build your own: DeepPartial
- A decision checklist
- Further reading
A few months ago I reviewed a PR where a junior had hand-written three nearly-identical interfaces: User, UserCreatePayload, and UserUpdatePayload. Same fields, three copies, drifting out of sync the moment someone added a column. I left one comment: type UserCreatePayload = Omit<User, "id" | "createdAt">. Two hundred lines became three. That is the whole pitch for utility types — they let one source of truth generate every shape your API, forms, and database layer actually need.
These ship in the standard library. No imports, no dependencies, documented in the TypeScript Handbook under "Utility Types." I have used every one of these in production this year. Here is where each one actually earns its keep — paired with the generics post, since these are generics' most useful instances.
Partial and Required: PATCH bodies and config
Partial<T> makes every property optional. Required<T> does the reverse. The textbook explanation is boring; the real use is PATCH endpoints and config merging.
A PATCH request, by definition, sends a subset of fields. You should never type the handler with the full entity — that lies about what the client sent. Partial models it exactly:
interface User {
id: string;
email: string;
displayName: string;
bio: string;
}
// A PATCH body is any subset of the mutable fields.
type UserPatch = Partial<Omit<User, "id">>;
function applyPatch(user: User, patch: UserPatch): User {
return { ...user, ...patch };
}
applyPatch(current, { bio: "Updated." }); // valid: one fieldRequired shines in the opposite scenario — config objects where users pass a loose shape and you normalize it internally. Your public API accepts Partial<Options>; after merging defaults, your internal code works with the fully-resolved Required<Options> and never has to null-check:
interface ServerOptions {
port?: number;
host?: string;
timeoutMs?: number;
}
const DEFAULTS: Required<ServerOptions> = {
port: 3000,
host: "0.0.0.0",
timeoutMs: 30_000,
};
function resolveOptions(opts: ServerOptions): Required<ServerOptions> {
return { ...DEFAULTS, ...opts };
}One caveat worth stating plainly: Partial is shallow. Nested objects stay required. That limitation is exactly why we build DeepPartial later.
Readonly: freeze the things that should never mutate
Readonly<T> marks every property immutable at the type level. It does nothing at runtime — Object.freeze is your runtime guard — but it catches the accidental reassignment before the code ships.
Where I reach for it: any module-level constant, anything returned from a selector, and React state objects that should only be replaced wholesale, never poked. The compiler error fires at the keystroke, not in a debugger three days later:
const FEATURE_FLAGS: Readonly<Record<string, boolean>> = {
newCheckout: true,
betaSearch: false,
};
FEATURE_FLAGS.newCheckout = false; // Error: Cannot assign to 'newCheckout'For arrays, ReadonlyArray<T> (or the shorthand readonly T[]) strips push, pop, and splice from the type. I default function parameters to readonly T[] when the function has no business mutating its input — which is most of them.
Pick and Omit: API DTOs from one source of truth
This is the pair I use most. Define your entity once, then derive every view of it.
Pick<T, Keys> keeps only the listed keys. Omit<T, Keys> drops them. The mental model: Pick is an allowlist, Omit is a denylist. Choose whichever produces the shorter, more stable list.
interface Product {
id: string;
sku: string;
name: string;
priceCents: number;
costCents: number; // internal — never leaves the server
createdAt: Date;
}
// Public API response: hide internal cost.
type ProductDTO = Omit<Product, "costCents">;
// A compact list-card view: allowlist exactly what the UI needs.
type ProductCard = Pick<Product, "id" | "name" | "priceCents">;
// Create payload: client supplies everything the server doesn't generate.
type ProductCreate = Omit<Product, "id" | "createdAt">;The rule I follow: use Omit when you are hiding a few sensitive or server-generated fields, and Pick when the consumer needs a small slice of a large entity. With Omit, adding a field to Product automatically includes it in the DTO — usually what you want for an API response, occasionally a privacy footgun if the new field is internal. With Pick, new fields stay out until you opt them in. For anything touching auth or PII, I lean Pick: explicit is safer than automatic.
| Operation | Style | New fields on T... | Best for |
|---|---|---|---|
Pick<T, K> | allowlist | excluded by default | small slices, PII-sensitive DTOs |
Omit<T, K> | denylist | included by default | hiding a few internal fields |
Record: typed maps and lookup tables
Record<Keys, Value> builds an object type with known keys and a uniform value type. It replaces the { [key: string]: Value } index signature with something that can be exhaustive.
The payoff is when Keys is a union. The compiler then forces you to cover every case — add a new status to the union and every Record keyed on it becomes a compile error until you handle it:
type OrderStatus = "pending" | "paid" | "shipped" | "cancelled";
const STATUS_LABELS: Record<OrderStatus, string> = {
pending: "Awaiting payment",
paid: "Paid",
shipped: "Shipped",
cancelled: "Cancelled",
// forget one and TS errors: Property 'cancelled' is missing
};I use this for status-to-color maps, role-to-permission tables, and route configs. It turns "did I handle every variant?" from a code-review question into a compiler guarantee.
Exclude and Extract: filtering unions
These two operate on union types. Exclude<T, U> removes members of T that are assignable to U; Extract<T, U> keeps only those that are. They are the set-difference and set-intersection of the type world.
type Status = "draft" | "published" | "archived" | "deleted";
// States a user can navigate to (no soft-deleted).
type VisibleStatus = Exclude<Status, "deleted">;
// "draft" | "published" | "archived"
type Event =
| { kind: "click"; x: number; y: number }
| { kind: "key"; code: string }
| { kind: "scroll"; delta: number };
// Pull one variant out of a discriminated union.
type KeyEvent = Extract<Event, { kind: "key" }>;
// { kind: "key"; code: string }Extract against a discriminated union is the trick most people miss — it is how you name a single variant without rewriting it. NonNullable<T> is just Exclude<T, null | undefined> with a friendlier name, and it is the right tool right after a guard:
function shout(label: string | null) {
if (label == null) return;
const safe: NonNullable<typeof label> = label; // string
console.log(safe.toUpperCase());
}ReturnType, Parameters, and Awaited: derive types from functions
This trio inverts the usual flow. Instead of writing a type and a function that conforms to it, you write the function and let the type follow. It is the single biggest boilerplate killer when you do not control the source — third-party libraries, generated clients, or your own factory functions.
ReturnType<typeof fn> extracts what a function returns. The canonical case is a config or store factory whose return type you never want to maintain by hand:
function createAppConfig() {
return {
apiUrl: process.env.API_URL ?? "http://localhost:3000",
retries: 3,
features: { search: true } as const,
};
}
// Stays correct no matter how the factory grows.
type AppConfig = ReturnType<typeof createAppConfig>;Parameters<typeof fn> grabs the argument tuple — useful for wrappers, memoizers, and logging shims that must forward exactly what the original takes. Awaited<T> unwraps a Promise (recursively, so nested promises collapse to one value), which is how you get the resolved type of an async function:
async function fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`);
return res.json() as Promise<{ id: string; email: string }>;
}
// The value you actually get after awaiting.
type User = Awaited<ReturnType<typeof fetchUser>>;
// { id: string; email: string }
// Forward arguments without restating them.
function withTiming<F extends (...a: never[]) => unknown>(fn: F) {
return (...args: Parameters<F>): ReturnType<F> => {
const start = performance.now();
const out = fn(...args) as ReturnType<F>;
console.log(`${fn.name}: ${(performance.now() - start).toFixed(1)}ms`);
return out;
};
}Before Awaited landed in TypeScript 4.5 (November 2021), people hand-rolled fragile T extends Promise<infer U> conditionals that broke on nested promises. Use the built-in; it handles the recursion and thenables correctly.
Build your own: DeepPartial
Eventually the built-ins run out. Partial is shallow, and configuration objects are deeply nested, so a partial config update needs recursion. Writing your own utility type is mostly stitching together keyof, mapped types, and conditional types — the same machinery the standard library uses:
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Settings {
theme: { mode: "light" | "dark"; accent: string };
editor: { fontSize: number; tabWidth: number };
}
// Override just one nested field — everything else stays optional.
const override: DeepPartial<Settings> = {
theme: { mode: "dark" },
};One honest caveat: this naive version treats arrays, Map, Date, and functions as plain objects and will recurse into them, which is rarely what you want. The production-grade version guards those:
type DeepPartial<T> = T extends (infer U)[]
? DeepPartial<U>[]
: T extends Date | Map<unknown, unknown> | Set<unknown> | Function
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;If you find yourself reaching for this often, type-fest already ships a battle-tested PartialDeep (and dozens more). Reach for a library before maintaining your own zoo of conditional types — but understand the mechanics first, because you will need them the day the library does not cover your case.
A light touch of template literal types deserves a mention here, because they compose with everything above. They let you build string-shaped types programmatically:
type Lang = "en" | "de" | "fr";
type RouteKey = `route.${Lang}.title`;
// "route.en.title" | "route.de.title" | "route.fr.title"
// Pairs naturally with Record for exhaustive i18n maps.
const titles: Record<RouteKey, string> = {
"route.en.title": "Home",
"route.de.title": "Startseite",
"route.fr.title": "Accueil",
};A decision checklist
When you catch yourself about to hand-write a type that resembles one you already have, run this list before typing a single property:
- Is it a subset of fields? Reach for
Pick(allowlist) orOmit(denylist). For PII, preferPick. - Is it a partial update or PATCH body?
Partial<T>, often wrapped aroundOmitto drop the id. - Is it a normalized config after defaults are merged?
Required<T>. - Is it a key-value map with known keys?
Record<Keys, Value>— and key it on a union for exhaustiveness. - Are you filtering a union?
Exclude/Extract, orNonNullableafter a null guard. - Does the type already exist on a function or value?
ReturnType,Parameters,Awaited,typeof. - Does it need to be immutable?
Readonly<T>plusObject.freezeat runtime. - Did the built-ins run short? Compose your own from
keyof+ mapped + conditional types, or pull intype-fest.
The throughline: define the entity once, derive everything else. A type you derived cannot drift from its source, because there is no second copy to drift. That single discipline is what made my User PR shrink from three interfaces to one — and it is why I read the utility-types section of the Handbook again every couple of years, since it keeps growing.
Further reading
- TypeScript Handbook — Utility Types (typescriptlang.org/docs)
- MDN Web Docs, for the underlying JavaScript semantics (developer.mozilla.org)
type-feston npm, for production-grade community utility types