Discriminated Unions on TypeScript


Imagine that we have a function that calculates the area of ​​a geometric figure and we need to create a Shape interface for this function. For now we will just focus on two basic figures: a circle, and a square.

Going back to some high school math, to calculate the area of a circle, we need to use the formula π * radius², or π * radius * radius. For a square, we need the sideLength. So, to calculate the area of a square, we can multiply the side length by itself: sideLength * sideLength.

How would we implement this in TypeScript? We could define a interface like this:

interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

In this interface, the kind property defines whether the shape is a circle or a square. Based on that, we could provide either the radius or the sideLength. But this doesn’t quite work, does it?

Shouldn’t the following example give us an error?

Let’s look at the following function:

function calculateArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
    // We got a error right here: 'shape.radius' is possibly 'undefined'.ts(18048)
  } else {
    return shape.sideLength ** 2;
    // And another one right here: 'shape.sideLength' is possibly 'undefined'.ts(18048)
  }
}

Why is that happening?

It’s because both radius and sideLength are optional parameters in the interface! You might only pass the kind property, since it’s the only required one. This means for the function, you could omit both radius and sideLength, because they are not required.

However, if you pass neither, you’ll encounter an error like this:

Argument of type '{}' is not assignable to parameter of type 'Shape'. Property 'kind' is missing in type '{}' but required in type 'Shape'.ts(2345).

Ok, Gabriel. But how do we fix this problem? Using Discriminated Unions!

We can define a new type called Circle that requires both a kind and a radius properties. Similarly, we can declare another type called Square, which also requires a kind but uses sideLength instead of radius. But here’s the key: for the Circle type, the kind property must strictly equal "circle”, and for the Square type, it must be "square”.

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

Great! Now we can create our Shape type!

type Shape = Circle | Square;

And look at that, no errors in our calculateArea function!

We even get a great autocomplete!

And if we try to pass an incorrect property, we’ll get a helpful error:

Object literal may only specify known properties, and 'radius' does not exist in type 'Square'.ts(2353)

Thank you for reading! I hope this article helps you better understand how to use discriminated unions in TypeScript. Happy coding!

For reference: