๐ŸŽ“ A1Learner
๐Ÿ”
0 of 42 lessons completed

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?