-
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
235: Conditional Types with Multiple Branches
Up until now, we've looked at conditional types as a simple "if-then-else" for types. But in the real world, logic is rarely that binary. Most of the time, you're dealing with a handful of different possibilities, not just two. In TypeScript, we handle this by chaining conditional types together, effectively creating a type-level "switch" statement.
Let's build something practical. Imagine we're writing a type-safe API wrapper. We have a set of "Action" strings, and depending on which action we pass to a function, we want the return type to change completely. If I pass 'GET_USER', I want a User object. If I pass 'GET_POST', I want a Post. If it's 'GET_COMMENT', I want a Comment.
Setting up the basic mappings
First, I'll define my data models and the available actions. I'll keep these simple so the type logic stays front and center.
type User = { id: string; name: string };
type Post = { id: string; title: string; content: string };
type Comment = { id: string; text: string };
type Action = 'GET_USER' | 'GET_POST' | 'GET_COMMENT';
Now, I want a type called ActionResponse that takes one of these actions as a generic parameter and spits out the corresponding model.
Chaining the conditions
Since we have three possible outcomes, a single T extends U ? X : Y isn't enough. I have to nest the next condition inside the "false" branch of the previous one.
type ActionResponse<T extends Action> =
T extends 'GET_USER'
? User
: T extends 'GET_POST'
? Post
: T extends 'GET_COMMENT'
? Comment
: never;
I personally find this indentation style helpful because it mimics the visual structure of an if/else if/else block. If you flatten it into one line, it becomes a nightmare to debug once you hit five or six branches.
The "Missing Fallback" mistake
Here is where I usually trip up when I'm rushing. I tried to implement this in a project last week and I did this:
// DON'T DO THIS
type ActionResponse<T extends Action> =
T extends 'GET_USER' ? User :
T extends 'GET_POST' ? Post :
Comment; // I just assumed the last one must be Comment
At first glance, this looks fine because T is constrained to Action. But this is a dangerous habit. If someone later adds 'GET_PROFILE' to the Action union but forgets to update the ActionResponse type, TypeScript will silently map 'GET_PROFILE' to Comment. That's a recipe for a runtime crash that the compiler should have caught.
By explicitly ending the chain with T extends 'GET_COMMENT' ? Comment : never, I'm telling TypeScript: "If it's none of the above, this is an invalid state." Using never as the final fallback ensures that if the union grows, the type system will scream at me until I handle the new case.
Putting it into a function
Now that the type logic is solid, we can use it in a function. This is where the magic actually happens for the end user of your code.
function apiCall<T extends Action>(action: T): ActionResponse<T> {
// Implementation details omitted for brevity
return {} as any;
}
const user = apiCall('GET_USER'); // Type is User
const post = apiCall('GET_POST'); // Type is Post
const comment = apiCall('GET_COMMENT'); // Type is Comment
You've now built a type-safe dispatcher. The compiler knows exactly what is coming back based on the string literal you passed in, without you having to manually cast types or use as User everywhere in your business logic.
📋 Practical Task
Building a Multi-Branch Permission Mapper
You are building a permission system for a dashboard. You have three roles: 'admin', 'editor', and 'viewer'. Each role should be mapped to a specific set of permissions.
'admin'should map to{ canEdit: true, canDelete: true, canView: true }'editor'should map to{ canEdit: true, canDelete: false, canView: true }'viewer'should map to{ canEdit: false, canDelete: false, canView: true }
Your Task:
- Define the three permission object types (or one type with different values).
- Create a union type
UserRolecontaining the three role strings. - Implement a conditional type
RolePermissions<T extends UserRole>using multiple branches (chained ternaries). - Ensure that the final fallback is
neverto prevent silent failures if new roles are added. - Create a function
getPermissions<T extends UserRole>(role: T): RolePermissions<T>and verify that calling it with'admin'returns the correct permission set.
There are no comments for now.