TypeScriptExclude & Extract

Exclude & Extract

Exclude<T, U> and Extract<T, U> are two of TypeScript's built-in distributive utility types that let you filter union types. They are mirror images of each other: Exclude removes members from a union, while Extract keeps only the matching members.

Exclude<T, U>

Exclude<T, U> produces a type by removing from union T any members that are assignable to U.

TS
type Status = 'active' | 'inactive' | 'pending' | 'banned';

// Remove 'banned' from the union
type AllowedStatus = Exclude<Status, 'banned'>;
// Result: 'active' | 'inactive' | 'pending'

// Remove multiple members
type PublicStatus = Exclude<Status, 'banned' | 'inactive'>;
// Result: 'active' | 'pending'

// Works with any union, not just strings
type NumOrStr = number | string | boolean | null;
type OnlyPrimitive = Exclude<NumOrStr, null>;
// Result: number | string | boolean
Note
Exclude removes members of the union that extend U. If no members match, the union is returned unchanged.
Extract<T, U>

Extract<T, U> is the opposite — it keeps only the members of union T that are assignable to U.

TS
type Status = 'active' | 'inactive' | 'pending' | 'banned';

// Keep only 'active' and 'pending'
type WorkingStatus = Extract<Status, 'active' | 'pending'>;
// Result: 'active' | 'pending'

// Extract string members from a mixed union
type Mixed = string | number | boolean | null | undefined;
type StringsOnly = Extract<Mixed, string>;
// Result: string

// Extract object-like types from a union
type Thing = string | number | { id: number } | { name: string };
type Objects = Extract<Thing, object>;
// Result: { id: number } | { name: string }
Tip
Think of Extract as a filter that keeps what matches, and Exclude as a filter that removes what matches.
How They Work: Distributive Conditional Types

Both Exclude and Extract are defined using distributive conditional types. The TypeScript built-in definitions are:

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

When T is a union, TypeScript distributes the conditional type over each member of the union individually, then recombines the results.

TS
// How Exclude<'a' | 'b' | 'c', 'a'> is evaluated:
// Step 1: distribute over each union member
//   'a' extends 'a' ? never : 'a'  =>  never
//   'b' extends 'a' ? never : 'b'  =>  'b'
//   'c' extends 'a' ? never : 'c'  =>  'c'
// Step 2: combine: never | 'b' | 'c'  =>  'b' | 'c'

// How Extract<'a' | 'b' | 'c', 'a' | 'b'> is evaluated:
//   'a' extends 'a' | 'b' ? 'a' : never  =>  'a'
//   'b' extends 'a' | 'b' ? 'b' : never  =>  'b'
//   'c' extends 'a' | 'b' ? 'c' : never  =>  never
// Combine: 'a' | 'b' | never  =>  'a' | 'b'

type Demo1 = Exclude<'a' | 'b' | 'c', 'a'>;         // 'b' | 'c'
type Demo2 = Extract<'a' | 'b' | 'c', 'a' | 'b'>;   // 'a' | 'b'
Exclude vs Extract: Side by Side

Utility

Formula

What it keeps

Exclude<T, U>

T extends U ? never : T

Members of T NOT in U

Extract<T, U>

T extends U ? T : never

Members of T that ARE in U

TS
type Colors = 'red' | 'green' | 'blue' | 'yellow';
type WarmColors = 'red' | 'yellow' | 'orange';

type CoolColors = Exclude<Colors, WarmColors>;
// 'green' | 'blue' — removed warm colors

type SharedColors = Extract<Colors, WarmColors>;
// 'red' | 'yellow' — only kept shared members
Practical Example: Filtering Event Types

A common use case is narrowing down a union of event types or action types to just the ones relevant for a handler.

TS
type AppEvent =
  | { type: 'USER_LOGIN'; userId: string }
  | { type: 'USER_LOGOUT'; userId: string }
  | { type: 'ITEM_ADDED'; itemId: string; quantity: number }
  | { type: 'ITEM_REMOVED'; itemId: string }
  | { type: 'CART_CLEARED' };

// Get only the user-related events
type UserEvent = Extract<AppEvent, { type: `USER_${string}` }>;
// { type: 'USER_LOGIN'; userId: string } | { type: 'USER_LOGOUT'; userId: string }

// Get only the item-related events
type ItemEvent = Extract<AppEvent, { type: `ITEM_${string}` }>;
// { type: 'ITEM_ADDED'; ... } | { type: 'ITEM_REMOVED'; ... }

// Remove cart events
type NonCartEvent = Exclude<AppEvent, { type: 'CART_CLEARED' }>;
// All events except CART_CLEARED

function handleUserEvent(event: UserEvent) {
  console.log('User event for:', event.userId);
}
Practical Example: Narrowing Union Members

TS
// A union with mixed types
type Input = string | number | boolean | Date | null | undefined;

// Keep only the "falsy-capable" types
type MaybeEmpty = Extract<Input, null | undefined | false>;
// null | undefined
// Note: false is not in Input, so it is filtered out

// Remove nullish values to get a "safe" type
type SafeInput = Exclude<Input, null | undefined>;
// string | number | boolean | Date

// A function that only accepts safe inputs
function process(value: SafeInput): string {
  if (value instanceof Date) return value.toISOString();
  return String(value);
}

// process(null);      // Error — null excluded from SafeInput
// process(undefined); // Error — undefined excluded from SafeInput
process('hello');   // OK
process(42);        // OK
Combining Exclude and Extract

TS
type Permissions = 'read' | 'write' | 'delete' | 'admin' | 'superadmin';

// Separate admin-level from regular permissions
type AdminPermissions = Extract<Permissions, 'admin' | 'superadmin'>;
// 'admin' | 'superadmin'

type RegularPermissions = Exclude<Permissions, AdminPermissions>;
// 'read' | 'write' | 'delete'

// Build role-based permission sets
type ViewerRole = Extract<RegularPermissions, 'read'>;
type EditorRole = Extract<RegularPermissions, 'read' | 'write'>;
type ManagerRole = RegularPermissions; // all regular permissions

function assignRole(user: string, role: RegularPermissions[]) {
  console.log(`${user} gets: ${role.join(', ')}`);
}

assignRole('Alice', ['read', 'write']); // OK
// assignRole('Bob', ['admin']);         // Error — 'admin' not in RegularPermissions
Extracting Event Handler Types

A particularly useful pattern is using Extract to derive handler types from a discriminated union, keeping your types in sync automatically.

TS
type Action =
  | { kind: 'fetch'; url: string }
  | { kind: 'save'; data: unknown }
  | { kind: 'delete'; id: number }
  | { kind: 'reset' };

// Derive a type for only fetch and save actions
type AsyncAction = Extract<Action, { kind: 'fetch' | 'save' }>;

// Map over the union to build a handler object type
type Handlers = {
  [A in Action as A['kind']]: (action: A) => void;
};

// Handlers is equivalent to:
// {
//   fetch:  (action: { kind: 'fetch'; url: string }) => void;
//   save:   (action: { kind: 'save'; data: unknown }) => void;
//   delete: (action: { kind: 'delete'; id: number }) => void;
//   reset:  (action: { kind: 'reset' }) => void;
// }

const handlers: Handlers = {
  fetch:  (a) => console.log('Fetching', a.url),
  save:   (a) => console.log('Saving', a.data),
  delete: (a) => console.log('Deleting id', a.id),
  reset:  ()  => console.log('Resetting'),
};
Exclude with never

When every member of a union is excluded, the result is never. When no members match, the original union is returned unchanged. Understanding these edge cases prevents surprises.

TS
// All members excluded — result is never
type Impossible = Exclude<'a' | 'b', 'a' | 'b'>;
// never

// No members match — result is unchanged
type Unchanged = Exclude<'a' | 'b', 'c' | 'd'>;
// 'a' | 'b'

// Extract with no overlap — result is never
type NoMatch = Extract<'a' | 'b', 'c' | 'd'>;
// never

// Using never as a signal for exhaustive checks
type Remaining = Exclude<'a' | 'b' | 'c', 'a' | 'b' | 'c'>;
// never — useful in exhaustiveness checks
function assertNever(x: never): never {
  throw new Error('Unexpected value: ' + x);
}
Warning
If Extract<T, U> returns never, it means T and U share no common members. Check your union types for typos.
Real-World: Narrowing API Response Types

TS
interface SuccessResponse {
  status: 'success';
  data: unknown;
}

interface ErrorResponse {
  status: 'error';
  message: string;
  code: number;
}

interface LoadingResponse {
  status: 'loading';
}

type ApiResponse = SuccessResponse | ErrorResponse | LoadingResponse;

// Extract only terminal states (not loading)
type SettledResponse = Exclude<ApiResponse, LoadingResponse>;
// SuccessResponse | ErrorResponse

// Extract only error response
type FailureOnly = Extract<ApiResponse, { status: 'error' }>;
// ErrorResponse

function handleSettled(response: SettledResponse) {
  if (response.status === 'success') {
    console.log('Data:', response.data);
  } else {
    console.error(`Error ${response.code}: ${response.message}`);
  }
}

function logError(response: FailureOnly) {
  console.error(`[${response.code}] ${response.message}`);
}
Success
You now understand how Exclude<T, U> removes union members and Extract<T, U> keeps matching members, how distributive conditional types power them, and how to apply them to filter API responses, events, permissions, and more.