-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
186: Exclude<Type, ExcludedUnion> and Extract<Type, Union>
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}!`);
}
There are no comments for now.