Discriminated Unions

A Common Tag Property

A discriminated union is a union of object types that all share one literal property (the discriminant, often type, kind or status). Checking it narrows to exactly one member. This is the most useful pattern in everyday TypeScript.

Example

type PaymentMethod =
  | { type: "upi"; vpa: string }
  | { type: "card"; last4: string; network: "VISA" | "RUPAY" | "MASTERCARD" }
  | { type: "netbanking"; bank: string }
  | { type: "cod" };

function describe(m: PaymentMethod): string {
  switch (m.type) {
    case "upi": return "UPI (" + m.vpa + ")";
    case "card": return m.network + " card ending " + m.last4;
    case "netbanking": return "Net banking via " + m.bank;
    case "cod": return "Cash on delivery";
  }
}

const methods: PaymentMethod[] = [
  { type: "upi", vpa: "ravi@okicici" },
  { type: "card", last4: "4242", network: "RUPAY" },
  { type: "netbanking", bank: "SBI" },
  { type: "cod" },
];
methods.forEach((m) => console.log(describe(m)));

Output

UPI (ravi@okicici)
RUPAY card ending 4242
Net banking via SBI
Cash on delivery

Accessing the Wrong Member

Example

Inside the "upi" branch there is no last4 (TS2339).

type PaymentMethod =
  | { type: "upi"; vpa: string }
  | { type: "card"; last4: string };

function describe(m: PaymentMethod): string {
  if (m.type === "upi") return m.last4;
  return m.last4;
}

Compiler output (tsc)

Line 6: error TS2339: Property 'last4' does not exist on type '{ type: "upi"; vpa: string; }'.

Request State

Example

Modelling loading state as a discriminated union makes impossible states (loading AND error at the same time) impossible to write. React apps use this constantly.

type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

function view(s: RequestState<string[]>): string {
  if (s.status === "loading") return "Spinner...";
  if (s.status === "error") return "Error: " + s.message;
  if (s.status === "success") return s.data.length + " products";
  return "Search for products";
}

console.log(view({ status: "idle" }));
console.log(view({ status: "loading" }));
console.log(view({ status: "success", data: ["Kettle", "Toaster"] }));
console.log(view({ status: "error", message: "Network down" }));

Output

Search for products
Spinner...
2 products
Error: Network down