Module 28: Interfaces and Type Design

Lesson 4 of 5 · 15–25 minutes

Discriminated unions

🏠The everyday picture

A parcel counter that handles letters, parcels and registered post differently. Each item carries a label saying which kind it is, and the clerk checks that label first.

📘What it really is

type Payment =
  | { method: "cash" }
  | { method: "upi"; upiId: string }
  | { method: "card"; last4: string };
function describePayment(p: Payment): string {
  switch (p.method) {
    case "cash": return "Paid in cash";
    case "upi":  return `Paid by UPI (${p.upiId})`;
    case "card": return `Paid by card ending ${p.last4}`;
  }
}

Inside case "upi", TypeScript knows upiId exists; using last4 there is an error.

    default: {
      const unhandled: never = p;   // error here if a kind is missing
      return unhandled;
    }

A switch that just does things (no return value) and has no such check gets no warning at all, so add the never check wherever a missed case would matter.

type Result<T> = { ok: true; value: T } | { ok: false; error: string };

(The <T> is a generic, explained in Module 29.)

🖱️Try it

Add the Payment type and describePayment. Then add a fourth method, "wallet" with a walletName, to the type only, and run npm run check. Read the error, then handle the new case.

✅Quick check

In a Payment union, what's the discriminant?

Show the answer

The method property, which has a different literal value for each kind.

A new payment method is added to the type. How does TypeScript help?

Show the answer

Wherever a switch has an exhaustiveness check (a never check in its default, or a declared return type), it reports that the new method isn't handled. Switches without such a check stay silent.

← PreviousType aliases and when to use which
ProLearners Assist