runtime.boot

Full Stack Systems
Production Mindset

CodeWithMihir

Engineering thoughtful products from interface to infrastructure.

CodeWithMihir

TypeScript Tutorial

TypeScript Discriminated Unions Explained

Learn how discriminated unions work in TypeScript using shared literal fields, switch statements, safe narrowing, and practical examples.

Welcome back! I am Mihir, and in this lesson we will learn discriminated unions in TypeScript.

A discriminated union is a union where each member has a shared property with a unique literal value.


Basic Discriminated Union

type LoadingState = {
  status: "loading";
};

type SuccessState = {
  status: "success";
  data: string[];
};

type ErrorState = {
  status: "error";
  message: string;
};

type RequestState = LoadingState | SuccessState | ErrorState;

The status property is the discriminator.


Narrowing with the Discriminator

function render(state: RequestState) {
  if (state.status === "success") {
    console.log(state.data);
  }

  if (state.status === "error") {
    console.log(state.message);
  }
}

When status is "success", TypeScript knows state has data.

When status is "error", TypeScript knows state has message.


Using switch

function getMessage(state: RequestState) {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return `Loaded ${state.data.length} items`;
    case "error":
      return state.message;
  }
}

switch works beautifully with discriminated unions.


Real Example: Payment

type PendingPayment = {
  kind: "pending";
  amount: number;
};

type CompletedPayment = {
  kind: "completed";
  amount: number;
  transactionId: string;
};

type FailedPayment = {
  kind: "failed";
  reason: string;
};

type Payment = PendingPayment | CompletedPayment | FailedPayment;

Usage:

function describePayment(payment: Payment) {
  switch (payment.kind) {
    case "pending":
      return `Pending: ${payment.amount}`;
    case "completed":
      return `Transaction: ${payment.transactionId}`;
    case "failed":
      return `Failed: ${payment.reason}`;
  }
}

Why This Pattern Is Powerful

Without a discriminator, you often need property checks.

With a discriminator, TypeScript has one clear field to inspect.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

This is easier to narrow than guessing based on random properties.


Choose a Clear Discriminator Name

Common names include:

  • type
  • kind
  • status
  • state

Pick one that matches your domain.


Quick Recap

  • A discriminated union uses a shared literal property.
  • The shared property is called the discriminator.
  • Checking the discriminator narrows the union.
  • switch statements are very readable with this pattern.
  • This is one of the best ways to model states in TypeScript.

Next up, we will learn Exhaustive Checks →.