Module 28: Interfaces and Type Design
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
- A discriminated union is a union of object types that each have a common property, the discriminant, with a different literal value:
type Payment =
| { method: "cash" }
| { method: "upi"; upiId: string }
| { method: "card"; last4: string };
- Checking the discriminant tells TypeScript exactly which shape you have:
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.
- Exhaustiveness: if someone adds
{ method: "wallet" }later, TypeScript can point to everyswitchthat doesn't handle it, but only where you've asked it to check. Two ways: a declared return type, as here (with the project'sstrictsetting, a missing case means the function can end without returning, which is an error), or, more reliably, an explicit check in thedefaultcase that only compiles when every kind is handled:
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.
- This pattern is perfect for things with several kinds or stages: payment methods, order states, API results:
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.
