Skip to content
JL

Home

About

Blog

Contact

Shop

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Utility Types Part 4: Exclude and Extract

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Utility Types Part 5: ReturnType and Parameters
TypeScript
9m
Sep 13, 2026

Utility Types Part 5: ReturnType and Parameters

ReturnType and Parameters are utility types that inspect function types rather than object types or unions. ReturnType extracts the type a function returns. Parameters extracts the types of its arguments as a tuple. Both are implemented using conditional types with infer, and both are essential for writing type-safe wrappers, decorators, and higher-order functions without duplicating type information.

#Utility Types#ReturnType+5
Utility Types Part 3: Pick and Omit
TypeScript
9m
Aug 30, 2026

Utility Types Part 3: Pick and Omit

Rather than transforming every property of a type — as Partial, Required, and Readonly do — Pick and Omit restructure a type by including or excluding specific named properties. Pick produces a type containing only the properties you name. Omit produces a type containing every property except the ones you name.

#Utility Types#Pick+5
Utility Types Part 2: Readonly and Record
TypeScript
10m
Aug 23, 2026

Utility Types Part 2: Readonly and Record

Two utility types that occupy opposite ends of the structural transformation space: Readonly locks an object's properties against mutation at the type level, while Record builds typed dictionary structures from a set of keys and a value type. Neither modifies optionality — they operate on shape and mutability instead.

#Utility Types#Readonly+5
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • Exclude and Extract Work on Unions Not Objects
  • What Exclude Does
  • How Exclude Is Implemented
  • What Extract Does
  • How Extract Is Implemented
  • Practical Patterns for Exclude
  • Practical Patterns for Extract
  • The Relationship Between Exclude and Extract
  • What Is Next
  • Key Takeaways
TypeScript

Utility Types Part 4: Exclude and Extract

September 6, 2026•10 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript Exclude and Extract utility types filtering union members at compile time
Utility Types Part 4: Exclude and Extract

The previous posts in this series covered utility types that transform object types: Partial, Required, Readonly, Record, Pick, and Omit. Each of those operates on the properties of an object type. Exclude and Extract are different — they operate on the members of a union type. Where Pick<T, K> selects properties from an object, Extract<T, U> selects members from a union. Where Omit<T, K> removes properties from an object, Exclude<T, U> removes members from a union. Understanding this distinction is the first step to using them effectively.

Exclude and Extract Work on Unions Not Objects

A union type like "a" | "b" | "c" is a type whose values can be any one of its members. When you have a union and need a subset of it, you could write the subset manually — but if the original union changes, you must update the subset by hand. Exclude and Extract let you derive union subsets from the original union, so they stay synchronised automatically.

Consider the difference between object manipulation and union manipulation:

// Object type manipulation (Pick/Omit)
type User = { id: number; name: string; email: string };
type UserPreview = Pick<User, "id" | "name">; // selects properties
 
// Union type manipulation (Extract/Exclude)
type Status = "active" | "inactive" | "suspended" | "deleted";
type ActiveStatus = Extract<Status, "active" | "inactive">; // selects members
type NonDeletedStatus = Exclude<Status, "deleted">; // removes members

Both pairs of utility types produce subsets, but they operate on entirely different type structures.

What Exclude Does

Exclude<T, U> takes a union type T and removes every member that is assignable to U. The result is the subset of T whose members cannot be assigned to U.

type Status = "active" | "inactive" | "suspended" | "deleted";
 
type LiveStatus = Exclude<Status, "deleted">;
// "active" | "inactive" | "suspended"
 
type WorkingStatus = Exclude<Status, "inactive" | "suspended" | "deleted">;
// "active"

Exclude works by assignability, not equality. A member is removed from T if it extends U. For simple string literal unions, assignability and equality amount to the same thing — "deleted" extends "deleted". But the assignability check matters when U is a broader type:

type Mixed = string | number | boolean | null | undefined;
 
type NonNullish = Exclude<Mixed, null | undefined>;
// string | number | boolean
 
type StringsOnly = Exclude<Mixed, number | boolean | null | undefined>;
// string

Exclude<T, null | undefined> is the standard way to remove nullish values from a union — and is in fact the implementation of the NonNullable<T> built-in utility type.

NonNullable Is Built on Exclude

NonNullable<T> is defined as Exclude<T, null | undefined>. It is a convenience alias for the most common use of Exclude. Knowing the implementation lets you apply the same pattern to custom sentinel values: Exclude<T, "loading" | "error"> strips specific string members the same way NonNullable strips null and undefined.

How Exclude Is Implemented

Exclude<T, U> is defined as:

type Exclude<T, U> = T extends U ? never : T;

This is a conditional type applied distributively over T. When T is a union, TypeScript evaluates the condition for each member of the union independently and collects the results. Members where T extends U is true produce never (which disappears from a union). Members where the condition is false produce T (the member itself). The final union is all the members that did not extend U.

// Distributing Exclude<"a" | "b" | "c", "b"> step by step:
// "a" extends "b" ? never : "a"  →  "a"
// "b" extends "b" ? never : "b"  →  never
// "c" extends "b" ? never : "c"  →  "c"
// Result: "a" | never | "c"  =  "a" | "c"

Distribution Requires a Naked Type Parameter

The distributive behaviour of Exclude and Extract depends on T being a bare (naked) type parameter — not wrapped in an array, tuple, or object type. T extends U ? never : T distributes over the union in T. But [T] extends [U] ? never : T does not distribute — it checks whether the entire union T extends U as a single type. This distinction matters when you write your own conditional utility types: if you want distribution, use a naked type parameter.

What Extract Does

Extract<T, U> is the complement of Exclude. It takes a union type T and keeps only the members that are assignable to U. Members that do not extend U are removed.

type Status = "active" | "inactive" | "suspended" | "deleted";
 
type TerminalStatus = Extract<Status, "suspended" | "deleted">;
// "suspended" | "deleted"
 
type StringOrNumber = string | number | boolean | null;
type OnlyPrimitives = Extract<StringOrNumber, string | number>;
// string | number

Extract is most useful when U is a broader type and you want all the members of T that fit within it:

type EventName = "click" | "focus" | "blur" | "keydown" | "keyup" | "submit";
 
type KeyboardEvent = Extract<EventName, `key${string}`>;
// "keydown" | "keyup"
 
type FormEvent = Extract<EventName, "focus" | "blur" | "submit">;
// "focus" | "blur" | "submit"

The template literal type `key${string}` in the first example shows that U can be any type — including template literal types. Every member of EventName that extends `key${string}` (i.e., starts with "key") is included in the result.

Extract with Interface Types for Discriminated Unions

Extract is particularly powerful with discriminated unions. If you have a union of objects with a kind discriminant, Extract<T, { kind: "foo" }> narrows the union to only the "foo" variant. This is cleaner than writing T extends { kind: "foo" } ? T : never inline, and more readable when used repeatedly across a codebase.

How Extract Is Implemented

Extract<T, U> is defined as:

type Extract<T, U> = T extends U ? T : never;

The logic is the mirror of Exclude. For each member of union T, if the member extends U, keep it (T). If not, discard it (never). After distribution, the result is the union of all members of T that extend U.

// Distributing Extract<"a" | "b" | "c", "a" | "c"> step by step:
// "a" extends "a" | "c" ? "a" : never  →  "a"
// "b" extends "a" | "c" ? "b" : never  →  never
// "c" extends "a" | "c" ? "c" : never  →  "c"
// Result: "a" | never | "c"  =  "a" | "c"

The implementation is just Exclude with the branches swapped. This symmetry makes the pair easy to remember: Exclude is "not extends, keep"; Extract is "extends, keep".

Practical Patterns for Exclude

Removing null and undefined. The most common use of Exclude in everyday TypeScript:

type MaybeString = string | null | undefined;
 
type DefiniteString = Exclude<MaybeString, null | undefined>;
// string

In practice, NonNullable<T> covers this case — but knowing it is Exclude under the hood lets you apply the same pattern to remove any unwanted member.

Removing sentinel values from enumerations. API response status types often include a loading or error state that is only meaningful in the UI layer, not in the business logic:

type RequestState = "idle" | "loading" | "success" | "error";
 
type CompletedState = Exclude<RequestState, "idle" | "loading">;
// "success" | "error"
 
function handleCompletion(state: CompletedState): void {
  // This function only runs after the request completes
  // TypeScript guarantees state is "success" | "error"
}

Filtering function overloads or method maps. When building a generic handler that should not apply to certain cases, Exclude narrows the key set:

type UserActions = "create" | "read" | "update" | "delete" | "export";
 
type MutatingActions = Exclude<UserActions, "read" | "export">;
// "create" | "update" | "delete"
 
type AuditLog = Record<MutatingActions, (userId: number) => void>;

The Record<MutatingActions, ...> enforces that the audit log covers exactly the write operations — no more, no less.

Exclude Keeps Derived Types in Sync

Like Omit for object types, Exclude keeps derived union types synchronised with their source. If you add "archive" to UserActions, the MutatingActions type does not automatically include it — but neither does it silently omit it. Any Record<MutatingActions, ...> literal will continue to work correctly because "archive" is not in MutatingActions. If you later add "archive" to MutatingActions by removing it from the excluded set, TypeScript immediately flags all incomplete Record literals. The synchronisation is explicit and compiler-enforced.

Practical Patterns for Extract

Narrowing discriminated unions. When a function should only handle a subset of variants:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };
 
type RectangularShape = Extract<Shape, { kind: "rectangle" | "triangle" }>;
// { kind: "rectangle"; width: number; height: number }
// | { kind: "triangle"; base: number; height: number }
 
function computeRectangularArea(shape: RectangularShape): number {
  if (shape.kind === "rectangle") return shape.width * shape.height;
  return 0.5 * shape.base * shape.height;
}

Extracting event subsets. In event-driven systems, different handlers care about different event categories:

type AppEvent =
  | { type: "user_created"; userId: number }
  | { type: "user_deleted"; userId: number }
  | { type: "post_published"; postId: number }
  | { type: "post_deleted"; postId: number }
  | { type: "comment_added"; commentId: number };
 
type UserEvent = Extract<AppEvent, { type: `user_${string}` }>;
// { type: "user_created"; userId: number } | { type: "user_deleted"; userId: number }
 
type PostEvent = Extract<AppEvent, { type: `post_${string}` }>;
// { type: "post_published"; postId: number } | { type: "post_deleted"; postId: number }
 
function handleUserEvent(event: UserEvent): void {
  // Only receives user-related events
}

The template literal type `user_${string}` matches any event whose type starts with "user_", selecting exactly the right subset of the union.

Extracting from mixed unions. When a union contains both primitive and object types:

type ConfigValue = string | number | boolean | { key: string; value: string }[];
 
type PrimitiveConfig = Extract<ConfigValue, string | number | boolean>;
// string | number | boolean
 
type ComplexConfig = Extract<ConfigValue, any[]>;
// { key: string; value: string }[]

The Relationship Between Exclude and Extract

Exclude and Extract are strict complements: Extract<T, U> keeps what Exclude<T, U> removes, and vice versa. For any union T and filter U, the members of Exclude<T, U> and Extract<T, U> together reconstruct the full union T (assuming no overlap in the filter).

This symmetry is useful for splitting a union into two non-overlapping parts:

type Permission = "read" | "write" | "admin" | "audit";
 
type WritePermissions = Extract<Permission, "write" | "admin">;
// "write" | "admin"
 
type ReadPermissions = Exclude<Permission, "write" | "admin">;
// "read" | "audit"

WritePermissions and ReadPermissions together contain all members of Permission with no duplication. Any future member added to Permission will appear in exactly one of the two derived types, depending on whether it is in "write" | "admin" or not.

Omit is also implemented in terms of Exclude: Omit<T, K> is Pick<T, Exclude<keyof T, K>>. The Exclude call removes the unwanted keys from keyof T, and Pick selects the remaining keys from T. Understanding Exclude therefore also deepens your understanding of how Omit works internally.

Exclude and NonNullable

NonNullable<T> is defined as T extends null | undefined ? never : T in older TypeScript versions and as Exclude<T, null | undefined> from TypeScript 4.8+. The two are equivalent because NonNullable was refactored to use Exclude once the standard library was updated. This is a good example of how utility types compose — a specialised utility type (NonNullable) is just a named instance of a more general one (Exclude) with fixed arguments.

What Is Next

Exclude and Extract complete the union-manipulation utility types. The next post covers ReturnType and Parameters — two utility types that operate on function types rather than object types or unions. ReturnType<F> extracts the type a function returns; Parameters<F> extracts the types of its parameters. Both are implemented using conditional types with infer, and both are essential for building type-safe wrappers and higher-order functions.

Key Takeaways

  • Exclude<T, U> removes every union member of T that extends U; Extract<T, U> keeps only the members that extend U — they are strict complements
  • Both are implemented as distributive conditional types: Exclude<T, U> = T extends U ? never : T; Extract<T, U> = T extends U ? T : never
  • Distribution happens because T is a naked (bare) type parameter — TypeScript evaluates the condition for each union member independently
  • NonNullable<T> is Exclude<T, null | undefined> — understanding Exclude explains how this built-in works
  • Omit<T, K> uses Exclude internally: Pick<T, Exclude<keyof T, K>> — the two utility type families are more closely related than they first appear
  • Use Exclude to strip unwanted members (nullish values, sentinel states, non-mutating actions); use Extract to select members that match a shape (discriminant value, naming pattern, structural constraint)