In TypeScript, what do the built-in utility types `Exclude<T, U>`, `Extract<T, U>` and `NonNullable<T>` produce when you apply them to a union type?
answer
- set operations over union members
- difference, intersection, drop nullish
- assignability decides, not equality
- rejected members become never
- erased — no runtime filtering
basics
~10 sThey are set operations over unions. Exclude<T, U> drops every member of T assignable to U, Extract<T, U> keeps only those members, and NonNullable<T> removes null and undefined from T.
solid answer
~40 sAll three filter the members of a union at compile time. `Exclude<T, U>` walks the union `T` member by member and throws away any member assignable to `U`, so `Exclude<'draft' | 'live' | 'archived', 'archived'>` is `'draft' | 'live'`. `Extract<T, U>` is the mirror image — it keeps only the members assignable to `U`, so `Extract<string | number | boolean, string>` is `string`. `NonNullable<T>` is the special case everyone reaches for under `strictNullChecks`: it strips `null` and `undefined`, so `NonNullable<User | null | undefined>` is `User`. Members that filter out contribute `never`, and `never` vanishes inside a union, which is why the result is just the surviving members. If nothing survives, the whole result is `never`. All of this is erased at compile time — no runtime filtering happens.
code
typescript · 13 linestype Status = 'draft' | 'live' | 'archived';
type Editable = Exclude<Status, 'archived'>; // 'draft' | 'live'
type Frozen = Extract<Status, 'archived' | 'deleted'>; // 'archived'
type Loaded = NonNullable<{ id: string } | null | undefined>; // { id: string }
// A non-union type behaves as a one-member union:
type Wide = Exclude<string, 'a'>; // string, not "string without 'a'"
type Narrow = Extract<string, 'a'>; // never — string is not assignable to 'a'
declare function render(status: Editable): string;
render('draft'); // ok
// render('archived'); // Error: 'archived' is not assignable to 'draft' | 'live'go deeper
Be able to state plainly what each of the three does to a union and give a one-line example, such as Exclude of a status union dropping 'archived'. Say clearly that they operate on union members.
Explain that the test is assignability rather than equality, that rejected members become never and disappear from the union, and that a fully filtered union collapses to never.
Show where these earn their keep in a codebase: deriving narrow parameter types from a single source-of-truth union so call sites fail early, and knowing that NonNullable is a claim to the checker, not a runtime guard.
Own the modelling question of whether a derived union should be computed with these operators or declared explicitly. Derivation keeps one source of truth but makes errors surface far from the declaration, so weigh readability against drift.
## The mental model: unions are sets A union type like `'draft' | 'live' | 'archived'` behaves like a set of three possibilities. `Exclude`, `Extract` and `NonNullable` are the set operations over that set: difference, intersection, and "remove the two nullish members". They live in TypeScript's standard library (`lib.es5.d.ts`), so they are always available without an import, and like everything in the type layer they exist only during compilation — the emitted JavaScript contains nothing from them. ## Exclude<T, U> — set difference `Exclude<T, U>` keeps the members of `T` that are **not** assignable to `U`: ```ts type Status = 'draft' | 'live' | 'archived'; type Editable = Exclude<Status, 'archived'>; // 'draft' | 'live' type NotFn = Exclude<string | number | (() => void), Function>; // string | number ``` The test is *assignability*, not string equality. `Exclude<string | number | (() => void), Function>` removes the function member because a `() => void` is assignable to `Function`. That also means excluding a supertype removes everything under it. One consequence surprises people: the filter is applied to each **member** of a union, and a non-union type is treated as a one-member union. So `Exclude<string, 'a'>` is `string` — `string` is not assignable to the literal `'a'`, so nothing is removed. It does *not* mean "string minus the letter a"; there is no such type. ## Extract<T, U> — set intersection `Extract<T, U>` is the complement: it keeps exactly the members `Exclude` would have thrown away. ```ts type Primitives = string | number | boolean | null; type Textish = Extract<Primitives, string | number>; // string | number type Nope = Extract<string, 'a'>; // never — string is not assignable to 'a' ``` The last line is the mirror image of the `Exclude<string, 'a'>` case and catches candidates out: direction matters. `Extract<'a' | 'b', string>` is `'a' | 'b'` because each literal *is* assignable to `string`, but `Extract<string, 'a'>` is `never` because the wide `string` is not assignable to the narrow `'a'`. ## NonNullable<T> — drop null and undefined ```ts type MaybeUser = { id: string } | null | undefined; type User = NonNullable<MaybeUser>; // { id: string } ``` This matters only when `strictNullChecks` is on; without it, `null` and `undefined` are already absorbed into every type and there is nothing to strip. In current TypeScript (the 5.x line) the standard library defines it as an intersection, `type NonNullable<T> = T & {}`, because the type `{}` means "any value except `null` and `undefined`". The observable behaviour is the same as removing the nullish members. ## Why the results look the way they do When a member fails the filter it becomes `never`, the empty type. `never` is the identity element of a union: `string | never` reduces to `string`. So filtering three members and rejecting one leaves `'draft' | 'live' | never`, which the checker immediately reduces to `'draft' | 'live'`. If **every** member is rejected you are left with `never` alone, and `never` has no values — the first assignment downstream will fail with a confusing error, which is usually the sign that a filter was too aggressive or a literal was misspelled. ## Where you actually use them - Deriving a narrower state union from a wider one: `type OpenTicket = Exclude<TicketState, 'closed' | 'archived'>`. - Pulling one variant out of a tagged union: `Extract<Action, { type: 'add' }>`. - Cleaning up an optional lookup result so downstream code is not forced to re-check for `null`. - Narrowing a callback parameter type after you have already validated it. ```ts function render(status: Exclude<Status, 'archived'>): string { return status === 'draft' ? 'Draft' : 'Live'; } ``` The compiler now rejects `render('archived')` at the call site, and the `switch` inside needs only two branches. ## The runtime boundary None of these does any filtering at run time. `NonNullable<T>` does not check for `null`; it only tells the checker that you have already ruled it out. If the value can actually be `null` at run time, you still need a real check — the utility type just stops the compiler from complaining, which is exactly the failure mode to avoid.
- What does `Exclude<string, 'a'>` evaluate to, and why does that surprise people?It evaluates to `string`. A non-union type is treated as a single-member union, and `string` is not assignable to the literal `'a'`, so nothing is removed. People read it as "string without the letter a", but there is no such type — `Exclude` only ever removes whole union members.
- What happens if every member of the union is filtered out?You get `never`. Each rejected member contributes `never`, and a union of nothing but `never` reduces to `never`. Nothing is assignable to `never`, so the first place you try to use the type produces an error that looks unrelated to the filter — usually the sign of a too-broad `U` or a misspelled literal.
- Does `NonNullable<T>` do anything if `strictNullChecks` is off?Effectively no. Without `strictNullChecks`, `null` and `undefined` are assignable to almost every type and are not tracked as separate union members, so there is nothing for it to strip. The utility is only meaningful in a strict codebase, which is one more argument for turning the flag on.
saying these in an interview costs you the question
- Thinking Exclude removes characters or keys, not union members
- Believing NonNullable adds a runtime null check
- Assuming Extract<string, 'a'> gives 'a' rather than never
- Saying these need an import from a library
- Expecting an error when the filtered type is unrelated