DEV Community

Gaurav Kumar Singh
Gaurav Kumar Singh

Posted on

Discriminated Unions in TypeScript

Earlier, we looked at how custom type guards (value is Type) let you manually inspect unknown data and teach TypeScript its shape. But what if you have a set of known object types that share a common property?

Enter Discriminated Unions (sometimes called tagged unions or algebraic data types). They are one of the most powerful patterns in TypeScript because TypeScript does the type narrowing for you automatically—no custom guard function required.

1. What is a Discriminated Union?

A discriminated union is a combination of three things:

  1. Types with a common property (the discriminant or "tag"), where each type has a unique literal type for that property.
  2. A union of those types.
  3. A conditional check (like a switch statement) on that discriminant property.

Let’s see it in action with a classic real-world scenario: asynchronous state management.

2. Practical Example: State Management

Imagine you are building a component that fetches data from an API. At any given moment, your state can be in one of three phases: Loading, Success, or Error.

If you lump everything into one giant interface with optional fields (data?: User, error?: string), you open the door to impossible states (like having both data and an error, or neither).

Instead, model them as a discriminated union using a status tag:

// 1. Define individual state shapes with a unique literal `status` property
interface IdleState {
  status: "idle";
}

interface LoadingState {
  status: "loading";
}

interface SuccessState {
  status: "success";
  data: { id: number; name: string }; // Only exists on success!
}

interface ErrorState {
  status: "error";
  error: Error; // Only exists on error!
}

// 2. Combine them into a Union type
type FetchState = IdleState | LoadingState | SuccessState | ErrorState;
Enter fullscreen mode Exit fullscreen mode

3. Automatic Narrowing in Action

When you consume a discriminated union, you can use a simple switch or if statement checking the status tag. Watch how TypeScript's control flow analysis automatically figures out what properties are safe to access:

function renderUI(state: FetchState) {
  // TypeScript knows `state` could be any of the 4 types

  switch (state.status) {
    case "idle":
      return "<p>Press button to fetch.</p>";

    case "loading":
      return "<p>Loading data...</p>";

    case "success":
      // 🚀 MAGIC: TypeScript automatically knows `state` is `SuccessState` here!
      // You have full type safety accessing `state.data`.
      return `<div>Welcome, ${state.data.name}</div>`;

    case "error":
      // TypeScript knows `state` is `ErrorState` here.
      // You have safe access to `state.error`.
      return `<p class="error">Error: ${state.error.message}</p>`;
  }
}
Enter fullscreen mode Exit fullscreen mode

No as SuccessState type assertions, no manual in checks, and no runtime ambiguity. The literal type of status tells TypeScript precisely which branch of reality your code is operating in.

4. Pro-Tip: Exhaustiveness Checking with never

What happens if you add a new state later (like RetryingState), but forget to update your switch statement? In JavaScript, your UI might silently fail or render nothing.

You can force TypeScript to catch missing cases at compile time using the never type:

function assertNever(x: never): never {
  throw new Error(`Unexpected object: ${JSON.stringify(x)}`);
}

function renderUI(state: FetchState) {
  switch (state.status) {
    case "idle": return "...";
    case "loading": return "...";
    case "success": return "...";
    case "error": return "...";

    // If you add a new union member and forget a case, 
    // TypeScript will throw a compile error right here because `state` 
    // won't narrow to `never`!
    default:
      return assertNever(state); 
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary

  • Custom Type Guards are your best tool when dealing with untrusted, unstructured data coming from the outside world (unknown inputs).
  • Discriminated Unions are your best tool when designing internal application logic, state machines, and data structures where you control the shapes.

Top comments (0)