TypeScript Discriminated Unions
A discriminated union is a union of object shapes that share one literal tag member. Checking the tag tells the checker exactly which shape a value has.
Narrowing with an if chain
Each comparison against kind selects one shape. Inside a branch, the checker allows the members of that shape only.
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Square | Triangle;
function describeShape(shape: Shape): string {
if (shape.kind === "circle") {
return "circle radius " + shape.radius;
}
if (shape.kind === "square") {
return "square side " + shape.side;
}
return "triangle base " + shape.base;
}
const first: Shape = { kind: "circle", radius: 3 };
const second: Shape = { kind: "square", side: 5 };
const third: Shape = { kind: "triangle", base: 6, height: 4 };
console.log(describeShape(first));
console.log(describeShape(second));
console.log(describeShape(third));Narrowing with a switch on the tag
A switch on kind gives one case per shape. The default branch logs a message for any value without a matching case.
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Square | Triangle;
function labelShape(shape: Shape): string {
switch (shape.kind) {
case "circle":
return "Circle";
case "square":
return "Square";
case "triangle":
return "Triangle";
default:
console.log("Unknown shape");
return "Unknown";
}
}
console.log(labelShape({ kind: "circle", radius: 3 }));
console.log(labelShape({ kind: "square", side: 5 }));
console.log(labelShape({ kind: "triangle", base: 6, height: 4 }));One function computing a value per shape
The area function returns a different calculation in each branch. Narrowing makes the members of each calculation available in that branch.
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Square | Triangle;
function shapeArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius * shape.radius;
}
if (shape.kind === "square") {
return shape.side * shape.side;
}
return (shape.base * shape.height) / 2;
}
console.log(shapeArea({ kind: "circle", radius: 3 }));
console.log(shapeArea({ kind: "square", side: 5 }));
console.log(shapeArea({ kind: "triangle", base: 6, height: 4 }));Example
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Square | Triangle;
function shapeArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius * shape.radius;
case "square":
return shape.side * shape.side;
case "triangle":
return (shape.base * shape.height) / 2;
default:
console.log("Unknown shape");
return 0;
}
}
const shapes: Shape[] = [
{ kind: "circle", radius: 1 },
{ kind: "square", side: 4 },
{ kind: "triangle", base: 6, height: 4 },
];
let total = 0;
for (const shape of shapes) {
total = total + shapeArea(shape);
}
console.log(total);Key points
- Each shape in the union carries the same tag member with a different literal value.
- Comparing the tag with a literal narrows the value to one shape.
- After narrowing, the members of that shape are safe to read.
- A switch on the tag handles each shape in its own case branch.
- The default branch of a plain switch runs when no case matches the tag.
In this example, a switch on the kind tag computes the area of each shape in an array, and the loop adds the three areas into one total.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท TypeScript Playground
Exercise
What makes the shapes in a discriminated union safe to narrow with a switch?