Skip to Content
Course content

186: Exclude<Type, ExcludedUnion> and Extract<Type, Union>

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was maintaining a complex state machine for a payment gateway. We had a union type called PaymentStatus that included everything from 'pending' and 'processing' to 'succeeded', 'failed', and 'refunded'. I had a specific function designed to handle "final" states—the ones where the transaction is effectively over. Initially, I just created a new union type called FinalStatus and listed the statuses manually. But then, the product team added 'cancelled' and 'disputed' to the main union. I forgot to update FinalStatus, and suddenly, our error handling logic was ignoring disputed payments. It was a classic case of "single source of truth" failure.

That's where Exclude and Extract come in. Instead of maintaining two separate lists that you have to keep in sync, these utility types allow you to derive one union from another. They let you tell TypeScript: "I want this type, but without these specific pieces," or "I only want the parts of this type that match these criteria."

Whittling Down Unions with Exclude

Exclude<Type, ExcludedUnion> is your tool for subtraction. It looks at the first union you provide and removes any members that are assignable to the second union. I find this most useful when it's easier to define what you don't want than what you do.

type UserRole = 'admin' | 'editor' | 'viewer' | 'guest';

// I want everything EXCEPT the 'guest' role for this specific permission check
type AuthenticatedRole = Exclude<UserRole, 'guest'>;

// AuthenticatedRole is now 'admin' | 'editor' | 'viewer'

The beauty here is that if we add a 'super-admin' role to UserRole later, AuthenticatedRole automatically picks it up. You've decoupled the "privileged" group from the "unprivileged" group without writing a second, redundant list.

Isolating Specifics with Extract

On the flip side, Extract<Type, Union> is for addition—or rather, filtration. It does the exact opposite of Exclude: it keeps only the members of the first union that are assignable to the second. While you might wonder why you wouldn't just write a new union, Extract is incredibly powerful when you're working with generic types or complex unions where you only want to "pluck" a few specific members that are guaranteed to exist in the original type.

type NotificationType = 'info' | 'success' | 'warning' | 'error';

// We only want to handle the types that actually require a user to take action
type ActionableNotification = Extract<NotificationType, 'warning' | 'error'>;

// ActionableNotification is 'warning' | 'error'

I often use Extract when I'm dealing with a large set of API response codes. If I have a union of 50 possible error codes, but one specific handler only cares about the 400-level "Client Error" codes, I can Extract those specifically. It keeps the logic tight and ensures that if a code is removed from the master list, the extracted type reflects that change immediately, preventing me from writing code for a state that no longer exists.




📋 Practical Task

Refactoring the Logistics Dispatcher

You are working on a shipping application. The system has a master union of PackageStatus, but different departments only care about specific subsets of those statuses. Currently, the developer has manually duplicated the unions, which is causing synchronization bugs.

Your Goal: Refactor the following code to use Exclude and Extract so that WarehouseStatus and CustomerStatus are derived from PackageStatus.

type PackageStatus = 
  | 'in-warehouse' 
  | 'picking' 
  | 'packed' 
  | 'shipped' 
  | 'out-for-delivery' 
  | 'delivered' 
  | 'returned';

// TODO: Refactor this to use Exclude. 
// WarehouseStatus should be everything EXCEPT 'shipped', 'out-for-delivery', 'delivered', and 'returned'.
type WarehouseStatus = 'in-warehouse' | 'picking' | 'packed';

// TODO: Refactor this to use Extract. 
// CustomerStatus should ONLY be 'shipped', 'out-for-delivery', and 'delivered'.
type CustomerStatus = 'shipped' | 'out-for-delivery' | 'delivered';

function updateWarehouseDisplay(status: WarehouseStatus) {
  console.log(`Package is currently in the warehouse: ${status}`);
}

function updateCustomerTracking(status: CustomerStatus) {
  console.log(`Your package is ${status}!`);
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.