DEV Community

Darren Hwang
Darren Hwang

Posted on

The most powerful pattern in TypeScript, Discriminated Unions

Discriminated Unions is useful in cases where you have a discriminant, which is a common property with a literal type (like 'idle', 'loading') that exists in every member.

TypeScript will narrow the type based on the discriminator, a common property to discriminate between union members. , making your code much safer.

type State = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: string }
  | { status: 'error'; error: Error }

function handleState(state: State) {
  switch (state.status) {
    case 'idle':
      // TypeScript knows state is { status: 'idle' }
      break
    case 'loading':
      // TypeScript knows state is { status: 'loading' }
      break
    case 'success':
      // TypeScript knows state has 'data' property
      console.log(state.data.toUpperCase())
      break
    case 'error':
      // TypeScript knows state has 'error' property
      console.error(state.error.message)
      break
    default:
      const _exhaustive: never = status
      throw new Error(`Unhandled status: ${status}`)
  }
}
Enter fullscreen mode Exit fullscreen mode

The most powerful patterns in TypeScript. Discriminated Unions allow you to create a type-safe state machine where the compiler ensures you only access data that actually exists in a given state.

Here is a breakdown of why this pattern is the gold standard for state management.


1. The Core Components

To create a Discriminated Union, you need three things:

  1. The Members: Individual object types representing different states.
  2. The Discriminant: A common property with a literal type (like 'idle', 'loading') that exists in every member.
  3. The Union: A type that combines them using the | operator.

Example Anatomy

type State = 
  | { status: 'idle' } 
  | { status: 'loading' } 
  | { status: 'success'; data: string } // 'data' only exists here
  | { status: 'error'; error: Error };   // 'error' only exists here
Enter fullscreen mode Exit fullscreen mode

2. Why It Beats "Optional Property" Hell

Without unions, developers often use one giant object with optional properties. This is dangerous because it allows for "impossible states."

The "Bad" Way (Optional Properties) The "Good" Way (Discriminated Unions)
data?: string; error?: Error; Data and Error are tied to specific statuses.
You could accidentally have both data and error at the same time. The type system makes it impossible to have data while in an error state.
Requires constant null checks or "non-null assertions" (!). TypeScript narrows the type automatically.

3. Type Narrowing in Action

As you showed in your post's switch statement, once you check the status property, TypeScript "narrows" the object to that specific member of the union.

function handleState(state: State) {
  if (state.status === 'success') {
    // Inside this block, TypeScript knows 'data' exists.
    // You don't need to check if state.data is undefined.
    console.log(state.data); 
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Exhaustiveness Checking

One of the best "pro tips" for state management is ensuring you've handled every possible state. You can use the never type to catch unhandled cases at compile time:

function handleState(state: State) {
  switch (state.status) {
    case 'idle': return 'Waiting...';
    case 'loading': return 'Loading...';
    case 'success': return state.data;
    case 'error': return state.error.message;
    default:
      // If you add a new state like 'processing' later, 
      // TypeScript will throw an error here because 'processing'
      // cannot be assigned to 'never'.
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck;
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary of Benefits

  • Safety: Prevents accessing data when the app is still loading.
  • Clarity: The code serves as documentation for what data is available when.
  • Maintainability: Adding a new state (e.g., reconnecting) triggers compiler errors in every function that hasn't accounted for it yet.

Further reading and references

  1. https://dev.to/shayy/20-typescript-tricks-every-developer-should-know-94c
  2. https://dev.to/maryanmats/stop-using-booleans-everywhere-use-union-types-instead-k9m

Top comments (0)