TypeScript – The Utility Types Worth Knowing

August 15, 20268 min readUpdated 9/5/2026

TypeScript ships a set of generic types that transform other types. They are ordinary generics — nothing magic — and knowing eight or nine of them removes most of the hand-written duplication from a codebase.

The reason to use them is always the same: a derived type cannot fall out of step with the type it came from, and a hand-copied one always eventually does.

Partial and Required

Partial<T> makes every property optional:

type ProductPatch = Partial<Product>;
// { id?: UUID; name?: string; description?: string; … }

Which is exactly the shape of a PATCH payload, or of an update function's argument:

function applyEdits(product: Product, edits: Partial<Product>): Product {
  return { ...product, ...edits };
}

Required<T> is the inverse, stripping every ?. Less common, but useful for the "after defaults have been applied" version of an options type:

function resolve(opts: RequestOptions): Required<RequestOptions> { /* … */ }

Both are shallow. Nested objects keep their original optionality — a deep version has to be written by hand, and lesson 15 shows how.

Pick and Omit

Two sides of the same coin. Pick keeps the keys you name:

type ProductSummary = Pick<Product, 'id' | 'name' | 'imageUrl'>;

Omit removes them:

type ProductDraft = Omit<Product, 'id' | 'createdAt' | 'updatedAt'>;

The pizza app uses Omit to stop a caller passing options the wrapper has already decided — and it is the clearest small example of a utility type doing real work:

export const api = {
  get: <T>(path: string, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
    request<T>(path, { ...opts, method: 'GET' }),

RequestOptions has method, body, auth and signal. api.get fixes the first two, so its opts parameter is the remaining two. Passing { method: 'POST' } to api.get is a compile error, which is the correct outcome and would have been impossible to express with a hand-written interface that did not track RequestOptions.

One difference worth knowing: Pick requires its keys to exist on T, while Omit does not. Omit<Product, 'nmae'> compiles and silently removes nothing. If a rename should break your derived types, Pick is the safer of the two.

Record

Builds an object type from a key type and a value type:

  fieldErrors(): Record<string, string> {
    const result: Record<string, string> = {};

That is the open-ended form — any string key — which is the index signature from lesson 6 written more briefly.

The far more interesting form is a closed key type:

const STATUS_LABEL: Record<OrderStatus, string> = {
  PENDING_PAYMENT: 'Awaiting payment',
  PAID: 'Paid',
  PREPARING: 'In the oven',
  COMPLETED: 'Completed',
  CANCELLED: 'Cancelled',
};

Now the object must have exactly those five keys — no more, no fewer. Add 'REFUNDED' to OrderStatus and this fails to compile until somebody writes the label:

Property 'REFUNDED' is missing in type '{ PENDING_PAYMENT: string; … }'
but required in type 'Record<OrderStatus, string>'.

This is the same guarantee the never exhaustiveness check gives a switch, and it is cheaper — one annotation instead of a default branch. Whenever you have a lookup keyed by a union, type it this way. The alternative fails silently, rendering a blank label for the new status.

ReturnType and typeof

ReturnType<F> extracts what a function type returns. Paired with typeof it lets you derive a type from a value, which is where it earns its place.

The pizza app's Redux store is the example, and the comment above it makes the argument better than I can:

/*
 * Types derived FROM the store rather than declared alongside it, so they can never drift out of
 * step with the reducers above. Add a slice and RootState grows automatically.
 */
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Read RootState from the inside out. store.getState is a function value; typeof store.getState is its type; ReturnType<…> is what it returns — which is the shape of the whole store, assembled from the four reducers.

Write that shape by hand and you have two descriptions of the same thing. Add a slice and one of them is wrong, with nothing to tell you.

The pre-typed hooks then hang off those two types:

export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();

Related: Parameters<F> gives a function's parameters as a tuple, and Awaited<T> unwraps a promise — Awaited<ReturnType<typeof fetchOrders>> is "whatever that async function resolves to", which is a common and genuinely useful combination.

Exclude, Extract and NonNullable

These three operate on unions rather than objects.

type Settled = Exclude<OrderStatus, 'PENDING_PAYMENT'>;
// 'PAID' | 'PREPARING' | 'COMPLETED' | 'CANCELLED'

type Active = Extract<OrderStatus, 'PAID' | 'PREPARING'>;
// 'PAID' | 'PREPARING'

type Url = NonNullable<Product['imageUrl']>;
// string  (was string | null)

Exclude removes members, Extract keeps them, NonNullable drops null and undefined. All three are conditional types over a union, which is why they distribute member by member — the behaviour mentioned in lesson 8.

NonNullable is the one you will use most, usually after a narrowing check has already established the value is there.

Readonly

type FrozenCart = Readonly<CartTotals>;

Every property becomes readonly. Shallow, like the others, and compile-time only — Object.freeze is the runtime version and they are unrelated.

The two string ones

Less used but occasionally exactly right: Uppercase, Lowercase, Capitalize and Uncapitalize transform string literal types.

type Loud = Uppercase<OrderType>;   // 'DELIVERY' | 'CARRYOUT' — already loud
type Event = `on${Capitalize<'select' | 'close'>}`;   // 'onSelect' | 'onClose'

That second one is a template literal type, and it is how libraries generate typed event-handler prop names from a list of events.

Writing the ones TypeScript does not ship

The built-in set is deliberately small. Three more are common enough that most codebases end up writing them, and each is a short exercise in the machinery from the next lesson.

Prettify — the most useful one nobody tells you about. Intersections and derived types are printed in error messages exactly as they were computed, so a hover can show you Omit<Product, 'id'> & { tempId: string } rather than the fields. This flattens it:

type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

It does nothing at all to the type — it maps every key to itself. What it changes is how the compiler displays it, because the result is a fresh object type rather than a record of how it was built. Wrap a confusing derived type in it and the tooltip becomes readable.

DeepPartial — because Partial is shallow, and a nested draft needs the whole tree optional:

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

Watch out for what counts as an object: arrays, Date and functions all do, so a naive version makes the elements of an array optional too. Production versions special-case those, which is a good illustration of why the built-in set stops where it does.

RequireAtLeastOne — for an options object where several fields are individually optional but at least one must be present:

type RequireAtLeastOne<T, K extends keyof T = keyof T> =
  Omit<T, K> & { [P in K]-?: Required<Pick<T, P>> & Partial<Omit<T, P>> }[K];

That one is genuinely hard to read, and it is included as an honest example of the ceiling. If your team cannot maintain a type, a runtime check and a comment may serve better. The line between "expressive" and "write-only" is real, and it is closer than enthusiasm suggests.

A caution about overloading the derived

Derived types have one failure mode worth naming. Chain enough of them and an error message becomes a paragraph, and the person reading it cannot tell which link is wrong:

type Thing = Partial<Omit<Pick<Product, 'name' | 'sizes'>, 'sizes'>>;

That is { name?: string }, arrived at by three steps that could have been zero. The rule of thumb: derive when the source type is likely to change and the derivation expresses a real relationship — a create payload really is "the entity without its server-assigned fields". Do not derive as a puzzle.

The list worth memorising

TypeDoesReach for it when
Partial<T>all optionala patch payload, or a form's draft state
Required<T>all requiredafter defaults are applied
Readonly<T>all readonlya value a function must not mutate
Pick<T, K>keeps keysa summary or list-row shape
Omit<T, K>drops keysa create payload, or fixing options
Record<K, V>builds an objecta lookup keyed by a union
Exclude<T, U>removes union membersa narrowed subset of a status
Extract<T, U>keeps union membersthe same, from the other side
NonNullable<T>drops null/undefinedafter a guard
ReturnType<F>a function's resultderiving a type from a value
Parameters<F>a function's argumentswrapping a function
Awaited<T>unwraps a promisewith ReturnType, on an async function

If you take one thing from the table, take Record<SomeUnion, T>. It is the cheapest way to make "we added a case and forgot to handle it somewhere" into a build failure, and that class of bug is otherwise found by users.

Where they come from

None of these is built into the compiler. Every one is declared in TypeScript's own library file in a few lines of ordinary type syntax, and you can read them:

type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Exclude<T, U> = T extends U ? never : T;
type Omit<T, K extends keyof never> = Pick<T, Exclude<keyof T, K>>;

That last line explains the Pick/Omit asymmetry noted earlier. Omit is Pick over Exclude<keyof T, K>, and Exclude silently ignores anything not present — so a misspelt key removes nothing rather than erroring.

The next lesson takes that syntax apart. The point of showing it here is that there is no magic layer: if the built-in set does not have what you need, you can write it in the same language.

One habit that follows from all of this: when you find yourself typing out a shape that already exists somewhere, stop and ask which utility would derive it. Most of the time one will, and the derived version is the one that still compiles after somebody renames a field.

Next

keyof, Mapped and Conditional Types — the machinery every one of these is built from, and how to write your own.