A union type A | B means a value can be either. Literal types restrict a value to exact options: "pending" | "paid" | "failed" is far safer than a plain string.
Before using a union, you narrow it: checks like typeof, in, instanceof or comparing a shared field tell TypeScript which member you have inside each branch.
A discriminated union gives every member a common literal field (often kind or type). Switching on that field narrows perfectly, and assigning the leftover case to never makes the compiler warn you if a new case is added but not handled.
type Payment =
| { kind: "mpesa"; phone: string; amount: number }
| { kind: "card"; last4: string; amount: number }
| { kind: "cash"; amount: number };
function describe(p: Payment): string {
switch (p.kind) {
case "mpesa":
return "M-Pesa from " + p.phone + ": " + p.amount + " TSh";
case "card":
return "Card ****" + p.last4 + ": " + p.amount + " TSh";
case "cash":
return "Cash: " + p.amount + " TSh";
default: {
const unhandled: never = p;
return unhandled;
}
}
}
function formatId(id: string | number): string {
return typeof id === "number" ? id.toString().padStart(6, "0") : id.toUpperCase();
}
console.log(describe({ kind: "mpesa", phone: "0712 345 678", amount: 15000 }));
console.log(formatId(42), formatId("inv-7"));Key points
- Prefer literal unions over plain strings for fixed sets of values.
- Narrow with
typeof,in,instanceofor a discriminant field. - The
nevercheck turns a forgotten case into a compile error.
Exercise
Model a Shape union of circle, rectangle and triangle (each with its own fields) and write area(shape). Add a fourth shape and confirm the compiler points to the switch that needs updating.
Show solution
Try the exercise yourself first — then compare your approach with this one.
Model the shapes as a discriminated union on kind. The switch narrows each case, and the never check in default means that adding a fourth shape (for example a square) makes the compiler point at this function until you handle it.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
default: {
const unhandled: never = shape;
throw new Error("Unknown shape: " + JSON.stringify(unhandled));
}
}
}
const shapes: Shape[] = [
{ kind: "circle", radius: 1 },
{ kind: "rectangle", width: 3, height: 4 },
{ kind: "triangle", base: 6, height: 2 },
];
for (const s of shapes) console.log(s.kind, area(s).toFixed(2));
// circle 3.14 / rectangle 12.00 / triangle 6.00