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 →.