Implementing utility types: Pick, Omit, Exclude, Awaited and friends from scratch
Write the built-in utility types yourself, test types with Expect<Equal<...>>, and learn why Omit breaks unions and how DistributiveOmit fixes it.
Comes up in: Coding
Learn#
Interviewers ask “implement Pick” because it shows whether you understand mapped types (TS.05.03) and conditional types (TS.05.02), not just their names. The built-ins are short:
Pick<T, K> is { [P in K]: T[P] }. Exclude<T, U> is T extends U ? never : T. Omit<T, K> is Pick<T, Exclude<keyof T, K>>.
To check a type, write a type test: a line that compiles only if two types are exactly equal, such as type t = Expect<Equal<MyPick<User, "id">, { id: number }>>. The practice below gives you Expect and Equal.
One famous trap: Omit on a union.
type Button = { kind: "button"; label: string; onClick: () => void };
type Link = { kind: "link"; label: string; href: string };
type Action = Button | Link;
type Plain = Omit<Action, "label">;
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
type Kept = DistributiveOmit<Action, "label">;
const a: Plain = { kind: "link", href: "/help" };
const b: Kept = { kind: "link", href: "/help" };
const c: Kept = { kind: "link", onClick: () => {} };example.ts(9,34): error TS2353: Object literal may only specify known properties, and 'href' does not exist in type 'Plain'.
example.ts(11,33): error TS2353: Object literal may only specify known properties, and 'onClick' does not exist in type 'Omit<Link, "label">'.Line 9 is wrong: plain Omit lost href. Line 11 is right: a link has no onClick.
State
- keyof (Button | Link) = "kind" | "label"
Action is Button | Link. Omit is not distributive, so it first takes keyof Action.
Read the steps as text
- Action is Button | Link. Omit is not distributive, so it first takes keyof Action.
- keyof a union gives only the keys every member shares. href and onClick are gone already.
- DistributiveOmit checks T extends unknown, which runs once per member.
- The results are joined: each member keeps its own keys.
Predict#
Predict first
What is keyof (A | B) when A = { a: 1; c: 1 } and B = { b: 1; c: 1 }?
A value of type A | B might be either, so only keys present in both are safe to read: "c". That is why non-distributive Omit drops member-specific keys. People expect all keys because a union “has more”.
Interview angle#
Say it like this — about 30 seconds
“Pick is a mapped type over the chosen keys: P in K, T[P]. Exclude is a distributive conditional type that turns matching members into never. Omit is Pick of T with Exclude of keyof T. ReturnType and Parameters use infer on a function type. Awaited unwraps recursively and also handles thenables. One trap: Omit is not distributive, so on a union it keeps only shared keys. DistributiveOmit wraps it in T extends unknown to apply it per member.”
Follow-up Implement Omit.
type MyOmit<T, K extends PropertyKey> = Pick<T, Exclude<keyof T, K>>;Follow-up Why is K in Omit not constrained to keyof T?
Follow-up How do you test types in CI?
Equal helper, checked by tsc --noEmit, or expectTypeOf in Vitest.Practice#
On your own
Implement six utility types. Each test compiles only when your type is exactly right.
Done when
MyPick,MyExcludeandMyOmitmatch the built-ins.MyParametersgives a tuple of the function’s parameter types.MyAwaitedunwraps nested promises and thenables.DistributiveOmitkeeps member-specific keys on a union.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
How is Omit built from other utility types?
Pick<T, Exclude<keyof T, K>>
What does keyof give for a union of object types?
Only the keys shared by every member.
How do you make Omit work per union member?
T extends unknown ? Omit<T, K> : never
Why is a naive Equal type risky in type tests?
It treats any as equal to everything, so wrong types pass.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Each built-in is one idea: Pick and Record are mapped types; Exclude, Extract, NonNullable, ReturnType and Parameters are conditional types; Omit combines both.
Check yourself: Write Exclude.
type MyExclude<T, U> = T extends U ? never : T;
How it works underneathSenior rounds
await unwraps any thenable, not only Promise, so the built-in Awaited matches { then(onfulfilled: infer F, ...args: infer _): any } and recurses on the value F is called with Source: TypeScript Handbook: Awaited.
Check yourself: Why does Awaited check for an object with then?
await works on any thenable, so the type must unwrap them too.
Edge cases and trapsWhere people slip
The Equal used here compares <T>() => T extends X ? 1 : 2 with the same for Y. TypeScript can only relate those when X and Y are identical, so any, readonly and ? differences all fail, as they should.
Check yourself: How does the strict Equal helper compare two types?
It compares two generic function types, which only TypeScript's internal identity check can match.
In productionStaff-level answers
Keep custom utility types in one shared file, each with type tests next to it. Run tsc --noEmit in CI so the tests fail the build. Vitest’s expectTypeOf is an alternative Source: Vitest: Testing types.
Check yourself: Where should a DistributiveOmit live in a codebase?
In a small shared types file, next to tests for it.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.