TypeScript Extract and Exclude: Filtering Union Types
TypeScript's union types are powerful, but they often contain more possibilities than you need in a specific context. Consider a typical API response type:
Key Insights
- Extract and Exclude are TypeScript utility types that filter union types by keeping or removing types assignable to a specified type, enabling precise type manipulation without manual type definitions.
- Exclude removes unwanted types from unions (like stripping null/undefined), while Extract does the opposite by selecting only matching types—understanding this distinction prevents common type system mistakes.
- These utilities shine in real-world scenarios like event handling systems, API response filtering, and discriminated unions where you need to programmatically narrow types based on specific criteria.
Why Union Type Filtering Matters
TypeScript’s union types are powerful, but they often contain more possibilities than you need in a specific context. Consider a typical API response type:
type ApiResponse =
| { status: 'success'; data: User[] }
| { status: 'error'; message: string }
| { status: 'loading' }
| { status: 'idle' };
// In your component, you only care about states with data
// How do you narrow this down without rewriting types?
This is where Extract and Exclude become essential. They let you programmatically filter union types, keeping your type definitions DRY and maintainable.
The Exclude Utility Type
Exclude<T, U> removes from type T all types that are assignable to type U. Think of it as a filter that says “give me everything except these.”
Here’s the actual TypeScript implementation:
type Exclude<T, U> = T extends U ? never : T;
This uses a conditional type with distribution. When T is a union, TypeScript distributes the conditional type over each member, effectively filtering out matches.
Practical Exclude Examples
The most common use case is removing null and undefined:
type NullableString = string | null | undefined;
type NonNullableString = Exclude<NullableString, null | undefined>;
// Result: string
// This is so common that TypeScript has NonNullable<T> as an alias
type AlsoNonNullable = NonNullable<NullableString>;
Filtering specific primitives from a union:
type MixedPrimitives = string | number | boolean | symbol;
type OnlyStringsAndNumbers = Exclude<MixedPrimitives, boolean | symbol>;
// Result: string | number
// Useful for function parameters
function processValue(value: OnlyStringsAndNumbers) {
// TypeScript knows value is definitely string | number
return typeof value === 'string' ? value.toUpperCase() : value * 2;
}
Excluding specific object shapes:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number }
| { kind: 'rectangle'; width: number; height: number };
type NonCircleShapes = Exclude<Shape, { kind: 'circle' }>;
// Result: { kind: 'square'; size: number } | { kind: 'rectangle'; width: number; height: number }
function calculateArea(shape: NonCircleShapes) {
// Circle is excluded, so we don't need to handle it
if (shape.kind === 'square') {
return shape.size * shape.size;
}
return shape.width * shape.height;
}
The Extract Utility Type
Extract<T, U> does the opposite—it keeps only types from T that are assignable to U. Think “extract matching types.”
Implementation:
type Extract<T, U> = T extends U ? T : never;
Notice it’s nearly identical to Exclude, just with the conditional branches flipped.
Practical Extract Examples
Extracting specific primitives:
type MixedTypes = string | number | boolean | (() => void);
type OnlyPrimitives = Extract<MixedTypes, string | number | boolean>;
// Result: string | number | boolean
type OnlyFunctions = Extract<MixedTypes, Function>;
// Result: () => void
This is particularly useful when working with discriminated unions:
type Action =
| { type: 'ADD_TODO'; payload: string }
| { type: 'REMOVE_TODO'; payload: number }
| { type: 'TOGGLE_TODO'; payload: number }
| { type: 'CLEAR_COMPLETED' };
// Extract only actions that have a payload
type ActionWithPayload = Extract<Action, { payload: any }>;
// Result: first three actions
// Extract specific action types
type TodoMutationAction = Extract<Action, { type: `${string}_TODO` }>;
// Result: all actions except CLEAR_COMPLETED
function handleMutation(action: ActionWithPayload) {
console.log('Processing payload:', action.payload);
// TypeScript knows action.payload exists
}
Real-World Applications
Event Handling Systems
Extract and Exclude excel at filtering event types:
type DomEvent =
| MouseEvent
| KeyboardEvent
| FocusEvent
| TouchEvent
| WheelEvent;
// Only handle pointer-based events
type PointerEvent = Extract<DomEvent, MouseEvent | TouchEvent>;
function handlePointerInteraction(event: PointerEvent) {
// TypeScript narrows to MouseEvent | TouchEvent
const x = 'clientX' in event ? event.clientX : event.touches[0].clientX;
const y = 'clientY' in event ? event.clientY : event.touches[0].clientY;
return { x, y };
}
// Exclude events we don't want to handle
type NonKeyboardEvent = Exclude<DomEvent, KeyboardEvent>;
function setupNonKeyboardListeners(element: HTMLElement) {
element.addEventListener('click', (e) => {
// e is automatically narrowed to appropriate type
});
}
API Response Type Narrowing
When working with complex API responses:
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
// Extract only states that indicate completion
type CompletedState<T> = Extract<
FetchState<T>,
{ status: 'success' } | { status: 'error' }
>;
function handleCompletion<T>(state: CompletedState<T>) {
if (state.status === 'success') {
return state.data; // TypeScript knows data exists
}
throw state.error; // TypeScript knows error exists
}
// Exclude terminal states to get pending states
type PendingState = Exclude<FetchState<any>, CompletedState<any>>;
function showLoadingIndicator(state: PendingState) {
// Only handles 'idle' | 'loading'
return state.status === 'loading' ? 'Spinner' : 'Placeholder';
}
Form Field Type Extraction
Building type-safe form libraries:
type FormField =
| { type: 'text'; value: string; maxLength?: number }
| { type: 'number'; value: number; min?: number; max?: number }
| { type: 'checkbox'; value: boolean }
| { type: 'select'; value: string; options: string[] }
| { type: 'date'; value: Date };
// Extract fields that need validation
type ValidatableField = Extract<
FormField,
{ maxLength?: number } | { min?: number } | { max?: number }
>;
function validateField(field: ValidatableField): boolean {
if ('maxLength' in field && field.maxLength) {
return field.value.length <= field.maxLength;
}
if ('min' in field && field.min !== undefined) {
return field.value >= field.min;
}
return true;
}
// Extract only fields with string values
type StringValuedField = Extract<FormField, { value: string }>;
function normalizeStringField(field: StringValuedField): string {
return field.value.trim().toLowerCase();
}
Advanced Techniques and Edge Cases
Nested Union Filtering
Extract and Exclude distribute over unions, which creates interesting behavior with nested types:
type NestedUnion =
| { outer: 'a'; inner: string | number }
| { outer: 'b'; inner: boolean | null };
// This might not work as expected
type Attempted = Extract<NestedUnion, { inner: string }>;
// Result: never (because no single union member has ONLY string)
// Better approach: filter the outer union first
type OuterA = Extract<NestedUnion, { outer: 'a' }>;
// Result: { outer: 'a'; inner: string | number }
Building Custom Utility Types
Combine Extract and Exclude to create specialized utilities:
// Extract types that have specific properties
type HasProperty<T, K extends PropertyKey> = Extract<
T,
Record<K, any>
>;
type Action =
| { type: 'A'; payload: string }
| { type: 'B'; meta: object }
| { type: 'C' };
type ActionsWithPayload = HasProperty<Action, 'payload'>;
// Result: { type: 'A'; payload: string }
// Exclude primitive types
type NonPrimitive<T> = Exclude<T, string | number | boolean | null | undefined | symbol | bigint>;
type MixedType = string | { name: string } | number | { id: number };
type OnlyObjects = NonPrimitive<MixedType>;
// Result: { name: string } | { id: number }
Performance and Limitations
These utilities are compile-time only—they have zero runtime cost. However, deeply nested or highly complex unions can slow down the TypeScript compiler:
// This can be slow with large unions
type HugeUnion = /* 100+ union members */;
type Filtered = Exclude<HugeUnion, /* complex condition */>;
// Consider breaking it down
type Step1 = Exclude<HugeUnion, /* simpler condition 1 */>;
type Step2 = Exclude<Step1, /* simpler condition 2 */>;
One gotcha: structural typing means Extract/Exclude match on shape, not identity:
type A = { x: number };
type B = { x: number }; // Same shape as A
type Union = A | B | { y: string };
type Result = Extract<Union, { x: number }>;
// Result: { x: number } (A and B are treated as identical)
Best Practices
Use Exclude for negative filtering: When you know what you want to remove, Exclude is more intuitive than listing everything you want to keep.
Use Extract for positive filtering: When you know exactly what you want, Extract makes intent clearer.
Prefer descriptive type aliases: Don’t inline complex Extract/Exclude operations:
// Bad
function handle(action: Extract<Action, { type: `${string}_TODO` }>) {}
// Good
type TodoAction = Extract<Action, { type: `${string}_TODO` }>;
function handle(action: TodoAction) {}
Consider alternatives for complex scenarios: If you’re nesting multiple Extract/Exclude operations, a discriminated union with type guards might be clearer.
Document non-obvious filtering: Add comments explaining why you’re filtering types, especially in shared codebases.
Extract and Exclude are fundamental tools for working with TypeScript’s type system. Master them, and you’ll write more maintainable, type-safe code with less duplication. They’re not just academic exercises—they solve real problems in event handling, API integration, and complex state management.