The TypeScript utility types I use

A hand-rolled mapped type in a code review sent me back to the handbook, because most of what we need already ships with TypeScript. These are the ones I use, plus a trap in Omit and the new NoInfer from 5.4.

A code review last week had a mapped type with a conditional inside, a keyword I had to look up, and a comment above it saying "makes every field optional except id". It worked. It was also Partial<User> & Pick<User, 'id'>, which TypeScript has had since version 2.1.

I'm not blaming the author, I wrote the same thing in 2019. The built-in utility types are easy to forget because they're not in the code you read every day, they're in lib.es5.d.ts and the handbook. So this is the list I use after a few years of React and Node, and the one that bites.

The ones for shapes

Partial<T> and Required<T>: every field optional, or every field required. Patch requests and form drafts are Partial, the final submit is Required.

Pick<T, K> and Omit<T, K>: a subset of fields, by naming the ones you want or the ones you don't. A UserSummary for a list is Pick<User, 'id' | 'name'>, a create request is Omit<User, 'id' | 'createdAt'>. I use these where I used to write a second type by hand and let it drift. The component types post leaned on Omit for the same reason.

Readonly<T>: for the props of a component that must not mutate what it's given, and for as const values you don't want anyone to touch.

Record<K, V>: an object with a fixed set of keys. Record<Tone, string> for a map from a union to CSS classes beats an index signature, because the compiler knows every key is present.

ts
// src/components/tones.ts
type Tone = 'neutral' | 'success' | 'danger';

const toneClass: Record<Tone, string> = {
  neutral: 'bg-gray-100',
  success: 'bg-green-100',
  danger: 'bg-red-100',
};
// leave one out and it's an error, add a fourth and it's an error

The ones for functions

ReturnType<typeof fn>, Parameters<typeof fn> and Awaited<T>: derive a type from a function instead of exporting one next to it.

ts
// src/server.ts
export async function startServer(port: number) {
  const server = createServer(app);
  await new Promise<void>((resolve) => server.listen(port, resolve));
  return { server, port, close: () => server.close() };
}

// in a test helper, no Started type was exported and I don't need one
type Started = Awaited<ReturnType<typeof startServer>>;
type Port = Parameters<typeof startServer>[0];

Awaited unwraps the promise, it arrived in 4.5. Before it, ReturnType of an async function gave you a Promise<...> and you wrote the unwrap yourself, badly. I'd still export a type in a real codebase when more than one file needs it, this is for the glue code where a function is the source of truth.

The ones for unions

Extract<U, M> and Exclude<U, M>: keep or drop members of a union. With a discriminated union they pick one case:

ts
type Action =
  | { type: 'add'; item: Item }
  | { type: 'remove'; id: string }
  | { type: 'clear' };

type AddAction = Extract<Action, { type: 'add' }>;
type ActionType = Action['type']; // 'add' | 'remove' | 'clear'

NonNullable<T>: strip null and undefined. Mostly after an optional chain, when you know the value is there and want the type to agree.

The trap in Omit

Omit on a union collapses it. keyof (A | B) is only the keys they share, so Omit<Action, 'type'> doesn't give you three objects without type, it gives you one empty object. The fix is a distributive version, which is the one homemade utility I keep in every project:

ts
// src/types/utils.ts
// T extends unknown makes the conditional distribute over each member of the union
export type DistributiveOmit<T, K extends keyof any> = T extends unknown ? Omit<T, K> : never;

The other thing about Omit: it takes any string as a key. Omit<User, 'nmae'> is not an error, it omits nothing and you don't find out. Pick is strict, it has to name real keys. So when a typo would be bad, prefer Pick.

The one I had to look up

Prettify, Matt Pocock's name for it. Not built in. It flattens an intersection so the hover shows { id: number; name: string } instead of Pick<User, 'id'> & { name: string }:

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

It changes nothing about the type, only how the editor prints it, and that's worth a lot in a code review.

New in 5.4

TypeScript 5.4 shipped two weeks ago with NoInfer<T>. It tells inference to skip a position:

ts
function createMachine<S extends string>(states: S[], initial: NoInfer<S>) {}

createMachine(['idle', 'loading'], 'idel');
// error: '"idel"' is not assignable to '"idle" | "loading"'

Without NoInfer, initial joins the inference and S quietly becomes 'idle' | 'loading' | 'idel'. I've wanted this exact thing for a form library and never knew what to call it.

The pull request from the start got merged with one line where the mapped type was. Check the handbook before you write one of your own.

Read more here: