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.
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
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.
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 }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.
// 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 |
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.
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
// 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); // OKCombining Exclude and Extract
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 RegularPermissionsExtracting Event Handler Types
A particularly useful pattern is using Extract to derive handler types from a discriminated union, keeping your types in sync automatically.
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.
// 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);
}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
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}`);
}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.