TypeScript
Completed
-
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
37: Distributive Conditional Types
You've already mastered basic conditional types—the T extends U ? X : Y syntax. On the surface, it looks like a simple ternary operator for types. But there is a "magic" behavior that kicks in when the type being checked is a union. TypeScript doesn't just check the union as a single block; it maps the condition over every single member of that union. We call this distributive conditional types.
Defining a messy event union
Let's build a real-world utility. Imagine we're building an event bus for a dashboard. We have a few different event types, and we often find ourselves needing to create types that exclude specific events to avoid handling them in certain listeners.
type AppEvent =
| { type: 'click'; x: number; y: number }
| { type: 'hover'; elementId: string }
| { type: 'keydown'; key: string }
| { type: 'resize'; width: number; height: number };
I want a utility where I can pass in AppEvent and a specific event type I want to get rid of, and have TypeScript return a union of everything else.
Writing the exclusion filter
At first glance, the logic seems trivial. If the type T extends the type we want to exclude (U), we return never. Otherwise, we return T.
type ExcludeEvent<T, U> = T extends U ? never : T;
// Let's try it out
type NoClickEvents = ExcludeEvent<AppEvent, { type: 'click'; x: number; y: number }>;
If this were a standard if/else statement in JavaScript, you'd expect it to check if the AppEvent union as a whole "extends" that one click object. That would be false, and the whole union would be returned. But if you hover over NoClickEvents, you'll see it's actually { type: 'hover'... } | { type: 'keydown'... } | { type: 'resize'... }. The click event is gone.
This is because TypeScript sees T is a union and effectively transforms the code into this:
({ type: 'click'... } extends U ? never : { type: 'click'... }) | ({ type: 'hover'... } extends U ? never : { type: 'hover'... }) ...
I love this because it makes Exclude (which is a built-in TS utility) possible. It's incredibly powerful for cleaning up unions.
The "Union Trap" mistake
Here is where I usually trip up when I'm moving fast. Sometimes, I don't want this distribution. I want to check the union as a single, atomic unit.
Let's say I want to write a helper that tells me if a type is "compatible" with a specific interface. I'll write a IsCompatible utility:
type IsCompatible<T, U> = T extends U ? true : false;
type MyEvents = { type: 'click' } | { type: 'hover' };
type Target = { type: 'click' };
type Result = IsCompatible<MyEvents, Target>;
// I expect 'false' because the whole union isn't just a 'click' event.
But when I check Result, I get true | false. Why? Because TypeScript distributed the check. It asked: "Is { type: 'click' } compatible with Target? Yes (true). Is { type: 'hover' } compatible? No (false)." Then it joined those results back into a union.
This is a classic bug. I'm asking a "Yes or No" question about the group, but TypeScript is giving me a "Yes or No" answer for every individual member.
Stopping the distribution
To fix this, we have to tell TypeScript: "Stop treating this as a list; treat it as a single wrapped package." We do this by wrapping the types in square brackets []. This turns the type into a tuple, and distribution only happens on "naked" type parameters.
type IsCompatible<T, U> = [T] extends [U] ? true : false;
type MyEvents = { type: 'click' } | { type: 'hover' };
type Target = { type: 'click' };
type Result = IsCompatible<MyEvents, Target> // now it's 'false'
By wrapping T and U in brackets, I've disabled the distributive behavior. Now, TypeScript compares the entire union MyEvents against the Target, which is exactly what I wanted. Keep this in your back pocket—whenever you see a conditional type returning a union of booleans instead of a single boolean, you've likely fallen into the distribution trap.
📋 Practical Task
Building a Non-Primitive Filter
You are building a validation library. You need a utility type called FilterPrimitives. This utility should take a union of types and remove any primitives (string, number, boolean, symbol, bigint) from that union, leaving only the objects, arrays, or functions.
Requirements:
- Create a union type called
Primitivescontaining the five basic TS primitives. - Implement the
FilterPrimitives<T>conditional type using distributive properties. - Test it with the following type:
type Mixed = string | number | { name: string } | boolean | { age: number }[]; - The resulting type should be
{ name: string } | { age: number }[].
There are no comments for now.