Why TypeScript Fails to Infer Types in Your Switch Statements

·

3 min read

Cover Image for Why TypeScript Fails to Infer Types in Your Switch Statements

TypeScript’s type inference system is incredibly powerful — but it also has strict rules designed to keep your code predictable and safe. Sometimes, those rules can feel overly cautious. A common example? Using a switch statement with discriminated unions and still getting a type error.

The Problem

Consider the following example:

type Action = 
  | { type: 'A'; payload: { body: string } } 
  | { type: 'B'; payload: { title: string } };

const handlers = { 
  A: (payload: { body: string }) => { /* ... */ }, 
  B: (payload: { title: string }) => { /* ... */ }, 
};

function handleAction(action: Action) { 
  switch (action.type) { 
    case 'A': 
      handlers[action.type](action.payload); // ❌ Error 
    case 'B': 
      handlers[action.type](action.payload); // ❌ Error 
  } 
}

You’ve narrowed action.type to 'A' | 'B'. So why is TypeScript still unhappy?

What TypeScript Sees

When you write handlers[action.type](action.payload), TypeScript sees:

  • action.type is 'A' | 'B'

  • action.payload is a union of all possible payload types

But TypeScript does not correlate the value of action.type with the shape of action.payload — because you're using dynamic property access (handlers[action.type]).

That means TypeScript treats the handler and the payload as unrelated, even if you just narrowed action.type a few lines earlier.

Why? Because tracking these kinds of correlations through dynamic keys is complex and fragile, especially in large codebases.

Why TypeScript Can’t Just “Fix” This

To fix this, TypeScript would need to:

  • Track the narrowing of action.type

  • Understand that it affects which key is accessed in handlers

  • Link that to the expected payload structure for that key

This means adding correlated type tracking across object access, which adds significant complexity to the compiler.

Instead, TypeScript takes the safe route: it doesn’t try to be clever with dynamic keys. It assumes you’re accessing the handler dynamically, and can’t guarantee the payload will match.

This is explained in detail in GitHub Issue #30581.

The Fix: Be Explicit

To avoid the error and keep things type-safe, restructure your switch like this:

type Action =  
| { type: 'A'; payload: { body: string } }  
| { type: 'B'; payload: { title: string } };

const handlers = {  
  A: (payload: { body: string }) => { /* ... */ },  
  B: (payload: { title: string }) => { /* ... */ },  
};

function handleAction(action: Action) {  
  switch (action.type) {  
    case 'A':  
      handlers.A(action.payload);  
      break;  
    case 'B':  
      handlers.B(action.payload);  
      break;  
  }  
}

Now you’re using direct property access, and TypeScript can clearly infer the type of both the handler and the payload. No more red squiggles.

Summary

TypeScript doesn’t infer types across dynamic object access because it avoids making risky assumptions. In a switch-case scenario with discriminated unions, if you’re using dynamic keys like handlers[action.type], the compiler can’t guarantee the payload matches — even if it logically makes sense to you.

TypeScript avoids correlating action.type and action.payload under dynamic access. This keeps the system safe, predictable, and maintainable. The workaround? Be explicit with direct property access in each case.

This may feel strict, but it ensures long-term reliability in complex codebases.