TypeScript Exclude and Extract
Exclude<T, U> and Extract<T, U> are two built-in TypeScript utility types that let you build a new union type by removing or keeping members of an existing union. They are opposites of each other: Exclude throws members out, Extract picks members in. Together they are the workhorses behind narrowing large union types — status enums, discriminated unions, event maps — down to exactly the subset you need, without retyping the members by hand.
Overview: How Exclude and Extract Work
Both utility types are defined using TypeScript’s distributive conditional types. When you write a conditional type whose checked type is a bare type parameter, and you pass in a union, TypeScript applies the conditional to each member of the union separately and unions the results back together. That single mechanic is the entire implementation of both utilities:
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
Read Exclude<T, U> as: "for every member of T, if it is assignable to U, replace it with never (which disappears from a union); otherwise keep it." Extract<T, U> does the mirror image: keep the member only if it is assignable to U, otherwise turn it into never.
Because never is the identity element for unions (X | never simplifies to X), the members that get mapped to never effectively vanish from the resulting type. This is exactly the same trick used internally to implement NonNullable<T>, which is really just Exclude<T, null | undefined>.
An important nuance: the second type argument U does not need to be a subset of T. TypeScript checks assignability, member by member, of each piece of T against the whole of U. This means you can pass a broader or unrelated type as U and TypeScript will simply keep or discard based on structural compatibility — which is powerful, but also the source of one of the most common mistakes covered below.
Syntax
type Removed = Exclude<UnionType, MembersToRemove>;
type Kept = Extract<UnionType, MembersToMatch>;
| Part | Meaning |
|---|---|
UnionType |
The source union type you are filtering (the first type argument, often called T). |
MembersToRemove / MembersToMatch |
The type each member of UnionType is tested against (the second type argument, often called U). Can itself be a union. |
Exclude<T, U> |
Result: every member of T that is not assignable to U. |
Extract<T, U> |
Result: every member of T that is assignable to U. |
Both are generic type aliases, so they only exist at the type level — you use them in type positions (after a colon, in a type alias, as a generic argument), never as runtime values.
Examples
Example 1: Filtering a literal union
The most common use case is trimming down a union of string literals, such as a status enum.
type Status = "pending" | "active" | "completed" | "cancelled";
type ActiveStatus = Exclude<Status, "cancelled" | "completed">;
type FinishedStatus = Extract<Status, "completed" | "cancelled">;
const current: ActiveStatus = "active";
console.log("Current status:", current);
const finished: FinishedStatus = "completed";
console.log("Finished status:", finished);
const allStatuses: Status[] = ["pending", "active", "completed", "cancelled"];
const activeOnly = allStatuses.filter(
(s): s is ActiveStatus => s !== "completed" && s !== "cancelled"
);
console.log("Active only:", activeOnly);
Output:
Current status: active
Finished status: completed
Active only: [ 'pending', 'active' ]
ActiveStatus collapses to "pending" | "active" and FinishedStatus collapses to "completed" | "cancelled". Notice the last part: Exclude/Extract only change the type. To actually filter an array of values at runtime you still need a real .filter() call — here paired with a type predicate (s is ActiveStatus) so the result array is correctly typed as ActiveStatus[] instead of staying Status[].
Example 2: Filtering a discriminated union of objects
Exclude and Extract also work beautifully on unions of object types, matched by a discriminant property.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
side: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Square | Triangle;
type CurvedShape = Extract<Shape, { kind: "circle" }>;
type StraightEdgedShape = Exclude<Shape, { kind: "circle" }>;
function describeShape(shape: StraightEdgedShape): string {
if (shape.kind === "square") {
return `Square with side ${shape.side}`;
}
return `Triangle with base ${shape.base} and height ${shape.height}`;
}
const mySquare: Square = { kind: "square", side: 4 };
const myTriangle: Triangle = { kind: "triangle", base: 6, height: 3 };
console.log(describeShape(mySquare));
console.log(describeShape(myTriangle));
const myCircle: CurvedShape = { kind: "circle", radius: 10 };
console.log("Circle radius:", myCircle.radius);
Output:
Square with side 4
Triangle with base 6 and height 3
Circle radius: 10
Here Extract<Shape, { kind: "circle" }> keeps only Circle, because it is the only member of Shape structurally assignable to { kind: "circle" }. Exclude does the opposite, leaving Square | Triangle. This is an extremely common pattern for narrowing a discriminated union to "everything except one case" without writing out the remaining members by hand — useful when the union has many variants.
Example 3: Stripping null and undefined
Exclude is also the standard tool for removing null/undefined from a type (this is literally how the built-in NonNullable<T> utility is implemented).
type ID = string | number | null | undefined;
type DefinedID = Exclude<ID, null | undefined>;
function printId(id: DefinedID): void {
console.log("ID:", id);
}
const rawIds: ID[] = ["abc123", 42, null, undefined, "xyz789"];
const definedIds = rawIds.filter(
(id): id is DefinedID => id !== null && id !== undefined
);
definedIds.forEach((id) => printId(id));
console.log("Total defined IDs:", definedIds.length);
Output:
ID: abc123
ID: 42
ID: xyz789
Total defined IDs: 3
DefinedID becomes string | number. printId can only be called with a value that is not null/undefined, and the compiler enforces that at every call site — the filter call with a type predicate is what actually removes the nullish values from the array at runtime, matching what the type promises.
Under the Hood
Because Exclude and Extract are just distributive conditional types, you can reimplement and verify them yourself. The compiler resolves the conditional against each union member independently, then re-unions the survivors. Once compiled to JavaScript, none of this exists anymore — all type information, including every Exclude/Extract computation, is erased at compile time. The emitted JS contains only the plain runtime statements (variable declarations, function calls, console.log), with zero trace of the union filtering logic.
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type Check1 = MyExclude<"a" | "b" | "c", "b"> extends Exclude<"a" | "b" | "c", "b">
? true
: false;
type Check2 = MyExtract<"a" | "b" | "c", "b" | "c"> extends Extract<"a" | "b" | "c", "b" | "c">
? true
: false;
const check1: Check1 = true;
const check2: Check2 = true;
console.log(check1, check2);
Output:
true true
Both hand-rolled versions behave identically to the built-ins, confirming there is no hidden magic: Exclude and Extract are ordinary library type aliases shipped in TypeScript’s default type declarations, not compiler intrinsics.
Common Mistakes
Mistake 1: Assuming Exclude keeps the excluded members
It is easy to misread Exclude<T, U> as "T, plus information about U" rather than "T with U’s matches removed". Trying to assign one of the removed members back in fails to compile:
type Status = "pending" | "active" | "completed" | "cancelled";
type ActiveStatus = Exclude<Status, "cancelled" | "completed">;
// Mistake: "completed" was excluded, it is no longer part of ActiveStatus
const s: ActiveStatus = "completed";
console.log(s);
tsc reports: Type '"completed"' is not assignable to type '"pending" | "active"'. — because ActiveStatus really did shrink down to just "pending" | "active". The fix is to only assign members that survived the exclusion:
type Status = "pending" | "active" | "completed" | "cancelled";
type ActiveStatus = Exclude<Status, "cancelled" | "completed">;
const s: ActiveStatus = "active";
console.log(s);
Mistake 2: Extracting with a criteria that doesn’t overlap
If none of the members of T are assignable to U, Extract<T, U> silently resolves to never — there is no error at the type alias itself, only once you try to use the (empty) result:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
side: number;
}
type Shape = Circle | Square;
// Mistake: "triangle" is not a member of Shape, so this silently becomes `never`
type TriangleOnly = Extract<Shape, { kind: "triangle" }>;
const bad: TriangleOnly = { kind: "circle", radius: 1 };
console.log(bad);
tsc reports: Type '{ kind: string; radius: number; }' is not assignable to type 'never'. The typo (or a discriminant that simply doesn’t exist in the union) goes unnoticed at the type TriangleOnly = ... line itself, and only surfaces later, often with a confusing "not assignable to never" error far from the real bug. Always double-check the discriminant value exists in the source union:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
side: number;
}
type Shape = Circle | Square;
type CircleOnly = Extract<Shape, { kind: "circle" }>;
const good: CircleOnly = { kind: "circle", radius: 1 };
console.log(good);
Best Practices
- Prefer
Exclude/Extractover retyping a smaller union by hand — when the source union changes, your derived type updates automatically. - When extracting from a discriminated union, match on the discriminant property (e.g.
{ kind: "circle" }) rather than duplicating the whole shape. - Remember these only change types. If you need to actually drop values from an array or object at runtime, pair them with a real
.filter()and a type predicate ((x): x is T => ...) so the runtime check and the compile-time type stay in sync. - After writing an
Extract<T, U>, hover the resulting type (or assign it to a throwaway variable) to confirm it isn’t accidentallynever— that usually signals a typo inU. - Use
NonNullable<T>instead of manually writingExclude<T, null | undefined>— it says the same thing more clearly, even though it is implemented withExcludeinternally. - Combine with
keyofto filter property names (e.g.Extract<keyof T, string>to drop numeric/symbol keys from an object type).
Practice Exercises
- Given
type Vehicle = "car" | "truck" | "motorcycle" | "bicycle" | "scooter";, write anExclude-based type aliasMotorVehiclecontaining every member except"bicycle"and"scooter". Then write anExtract-based type aliasHumanPoweredcontaining only"bicycle"and"scooter". - Given a discriminated union
type Event = { type: "click"; x: number; y: number } | { type: "keypress"; key: string } | { type: "scroll"; deltaY: number };, useExtractto derive a type calledPointerEventthat matches only the"click"variant, and write a function that accepts aPointerEventand logs itsxandycoordinates. - Given
type Value = string | number | boolean | (() => void);, useExtractwith the built-inFunctiontype to derive a typeCallablethat contains only the function member, and useExcludeto deriveNonCallablecontaining the rest. What type do you expectCallableto resolve to?
Summary
Exclude<T, U>removes from unionTevery member assignable toU;Extract<T, U>keeps only the members assignable toU.- Both are implemented with distributive conditional types:
T extends U ? never : TandT extends U ? T : neverrespectively — a member mapped toneverdisappears from the resulting union. - They work on literal unions as well as unions of object types, where matching is based on structural assignability (great for discriminated unions).
- They only affect types — all type information is erased at compile time, so filtering real array/object data at runtime still requires an explicit
.filter()or equivalent check, ideally with a type predicate. - An
Extractwhose criteria doesn’t overlap with the source union silently producesnever; watch for "not assignable to never" errors as a symptom of a typo’d discriminant. NonNullable<T>is justExclude<T, null | undefined>under the hood.
