Write the one-line definitions of TypeScript's built-in `Exclude<T, U>` and `Extract<T, U>` yourself, and explain why they filter a union member by member instead of testing the union as a whole.
answer
- one conditional type each
- mirror branches, never on the discard side
- bare type parameter, applied per member
- never vanishes inside a union
- swap branches to get the other
basics
~20 sExclude<T, U> is T extends U ? never : T and Extract<T, U> is T extends U ? T : never. Because T is a bare type parameter, the conditional is applied to each union member separately and the results are unioned.
solid answer
~40 sBoth are single conditional types, and they are exact mirrors of each other: `type Exclude<T, U> = T extends U ? never : T` and `type Extract<T, U> = T extends U ? T : never`. The filtering behaviour comes from the fact that `T` appears bare on the left of `extends`, so the checker applies the conditional to each member of the union independently and unions the results — a rejected member yields `never`, and `never` disappears when unioned with anything else. That is the whole trick: `Exclude<'a' | 'b', 'a'>` becomes `never | 'b'`, which reduces to `'b'`. Being able to write these two lines from memory is the standard way interviewers check that a candidate understands conditional types rather than just memorising the utility list.
code
typescript · 15 linestype MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type Status = 'draft' | 'live' | 'archived';
// 'draft' -> 'draft', 'live' -> 'live', 'archived' -> never
type Editable = MyExclude<Status, 'archived'>; // 'draft' | 'live'
type Frozen = MyExtract<Status, 'archived'>; // 'archived'
// A too-wide filter swallows everything, because every type extends unknown:
type Gone = MyExclude<Status, unknown>; // never
// Wrapping the parameter turns the per-member behaviour off:
type Whole<T, U> = [T] extends [U] ? never : T;
type Unfiltered = Whole<Status, 'archived'>; // Status — no filtering happenedgo deeper
Memorise the two one-liners and be able to reproduce them on a whiteboard. Check yourself against a two-member example so you do not swap the branches under pressure.
Explain why never is used for the rejected branch and how a union containing never reduces, and demonstrate the evaluation member by member for a three-member union.
Show that you can build the variant a real codebase needs from the same shape rather than reaching for a helper library, and diagnose a filter that unexpectedly returns never or the untouched union.
Set the norm for how far a team goes writing custom conditional types. Each one is code future readers must decode, so favour the standard library's names where they fit and keep bespoke type-level helpers few, named clearly, and covered by type tests.
## The two definitions Both types are one line each in TypeScript's standard library: ```ts type Exclude<T, U> = T extends U ? never : T; type Extract<T, U> = T extends U ? T : never; ``` Read them literally: "if `T` is assignable to `U`, produce `never`, otherwise produce `T`" and the reverse. They differ only in which branch keeps the type and which throws it away, so if you can write one you can write the other by swapping the branches. ## What `extends` means here Inside a conditional type, `A extends B` is not class inheritance. It is the assignability question: *can a value of type A be used where a B is expected?* `'draft' extends string` is true; `string extends 'draft'` is false; `() => void extends Function` is true. Understanding that the test is assignability, not equality, explains most of the surprising results — `Exclude<string | number, unknown>` is `never`, because everything is assignable to `unknown`. ## Why the filter runs per member When the type being tested is a bare type parameter — `T` written by itself immediately to the left of `extends`, not wrapped in anything — and the type substituted for it is a union, the checker applies the conditional once per member and unions the results. So evaluating `Exclude<'a' | 'b' | 'c', 'a'>` is really three evaluations: ``` 'a' extends 'a' ? never : 'a' -> never 'b' extends 'a' ? never : 'b' -> 'b' 'c' extends 'a' ? never : 'c' -> 'c' ``` unioned back together as `never | 'b' | 'c'`. Because `never` is the empty type, a union containing it reduces to the other members, so the final answer is `'b' | 'c'`. That reduction is why `never` is the right "delete me" marker: any other choice would leave debris in the result. If the conditional tested the union as a whole instead, `'a' | 'b' | 'c' extends 'a'` would simply be false and you would get the entire union back unchanged — which is exactly what happens to the naive first attempt candidates often write. Filtering *requires* the per-member evaluation. ## Writing your own variants Once you have the shape, the useful variants come cheaply: ```ts type MyExclude<T, U> = T extends U ? never : T; type MyExtract<T, U> = T extends U ? T : never; type MyNonNullable<T> = T extends null | undefined ? never : T; type Falsy<T> = Extract<T, '' | 0 | false | null | undefined>; ``` `Falsy` shows the composition style: you rarely need a new conditional, you just pick a different `U`. Note that `MyNonNullable` written as a conditional is how the standard library used to define it; in the TypeScript 5.x line the library instead uses the intersection `type NonNullable<T> = T & {}`, where `{}` means "any value that is not `null` or `undefined`". Both give the same answer for concrete unions. ## The relationship between the two `Exclude` and `Extract` partition the union: for any `T` and `U`, every member of `T` ends up in exactly one of `Exclude<T, U>` and `Extract<T, U>`. That duality is worth saying out loud in an interview, and it is also a practical debugging tool — if `Extract<T, U>` is `never`, then `Exclude<T, U>` is the whole of `T`, which tells you your `U` matched nothing. ## Common mistakes when writing them cold - Swapping the branches, so `Exclude` keeps what it should drop. Sanity-check with a two-member union before moving on. - Returning `undefined` or `void` instead of `never` in the discard branch. Neither disappears from a union, so `Exclude<'a' | 'b', 'a'>` would come back as `undefined | 'b'`. - Wrapping the parameter — writing `[T] extends [U] ? ... : ...` — which deliberately turns the per-member behaviour off and makes the whole union get tested at once. That is a real technique for other purposes, but it breaks a filter. - Adding a constraint like `U extends T` and assuming that is what the library does. It is not; the built-ins leave `U` completely unconstrained. ## Why interviewers ask this one It is the cheapest possible probe of conditional types: two lines, no cleverness, and a candidate either knows the shape or does not. It also opens the door to the follow-up that actually separates people — what `never` does inside a union, and what happens when the filter removes everything.
- Why is `never` the right type for the discarded branch rather than `undefined`?Because `never` is the empty type and is absorbed by a union: `never | 'b'` reduces to `'b'`. `undefined` is a real type with a value, so it would survive and pollute every result — `Exclude<'a' | 'b', 'a'>` would come back as `undefined | 'b'` instead of `'b'`.
- How would you write `NonNullable<T>` as a conditional type?`type MyNonNullable<T> = T extends null | undefined ? never : T`. Both nullish types go in the filter so both are removed. The current standard library instead defines it as the intersection `T & {}`, since `{}` accepts every value except `null` and `undefined`; for concrete unions the two definitions agree.
- What does `Exclude<'a' | 'b', unknown>` evaluate to, and what does that tell you about the test being used?`never`. Every type is assignable to `unknown`, so both members pass the `extends` test and both are discarded. It shows the check is assignability against `U`, not equality with it — a wide `U` silently swallows the whole union.
saying these in an interview costs you the question
- Writing the conditional with the branches swapped
- Using undefined or void instead of never to discard
- Claiming U is constrained to extend T
- Saying the union is tested as a single type
- Thinking the utilities are compiler magic, not library types