Skip to Content
Course content

235: Conditional Types with Multiple Branches

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

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:

  1. Define the three permission object types (or one type with different values).
  2. Create a union type UserRole containing the three role strings.
  3. Implement a conditional type RolePermissions<T extends UserRole> using multiple branches (chained ternaries).
  4. Ensure that the final fallback is never to prevent silent failures if new roles are added.
  5. Create a function getPermissions<T extends UserRole>(role: T): RolePermissions<T> and verify that calling it with 'admin' returns the correct permission set.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.