Did you know about Discriminated Unions in TypeScript?

Mhetre Ayush
6 min readDec 29, 2023

--

Instead of diving straight into the theory, let us take a look at a practical example:

Requirements:

  • You need to create a function that returns one of the 3 shapes, namely Square, Rectangle, and Circle.
  • The function should accept only the relevant parameters.
  • The parameters for each shape are different and they are:
    — Circle: radius
    — Square: size
    — Rectangle: height & width

One of the easiest ways to do this in TypeScript is to create a type as follows:

type CustomShapeProps = {
kind: "square" | "rectangle" | "circle";
size?: number;
width?: number;
height?: number;
radius?: number;
};

This would work but do you see any flaws in this code? Take a look at the following GIF to see how the code using this type would work:

Autocomplete for type without Discriminated Union

As you can see, all the parameters are acceptable regardless of the “kind” of the shape.
What if we compile this TypeScript code? But before we get to the results, let us see what the code for the function “getCustomShape” looks like:

Code for getCustomShape function:

code for the getCustomShape function

The results on compiling the code:

Result without discriminated Unions

According to the requirements:

The function should accept only the relevant parameters

With our knowledge of the results and the above-mentioned point we can conclude that:

  • Not only the “radius” property which does not exist on the “square” shape can be passed without any errors/warnings, but also, the passing of the “size” property which is required by the “square” shape is not mandatory.
  • Let us take a look at a possible real-life debugging session:
//Debugging all the parts of the code where the parameters are used.
console.log("CustomShape.tsx -> Line 874", {radius}) // radius: undefined
console.log("CustomShape.tsx -> Line 591", {radius}) // radius: undefined
console.log("CustomShape.tsx -> Line 369", {radius}) // radius: undefined

// Only to learn:
// The shape was initially of kind "square" and hence
// was recieving the prop "size"
// but you needed to make it a "circle"
// you changed the "kind" to "circle" but
// you did not update the "props" being passed
// and neither were you screamed at by the TypeScript Compiler
// Hence, "radius" is "undefined" everywhere

// console.log({radius}) is written to
// print both, the name as well as the value of the radius
// This is to achieve what the following line achieves:
// console.log("radius", radius)
  • The “size” parameter, is of type number | undefined but according to our requirements, we always want it to be defined and of the type number if the kind is “square”, this also applies to all the other parameters namely “radius”, “height” & “width” with respect to the “kind”.

Okay, now that we know the problem, let us take a look at one of the easiest solutions:

Discriminated Unions in TypeScript:

Let us first take a look at the results and the code before learning what this is and how to implement it.

Result with Discriminated Unions:

Result with discriminated Unions

The IDE behavior:

Autocomplete with Discriminated Union:

Note:

*: The content of the sections marked with “*” was generated using ChatGPT for it to be explained in the simplest words.

* In the simplest way, here is what discriminated unions mean:

  • In TypeScript, discriminated unions are a way to combine different types into a single type that can represent multiple possibilities.
  • This is achieved by adding a common property to each type in the union, called a discriminant, which helps TypeScript narrow down the possible types within the union.

Let us understand this with the shape example that we have been using throughout the article:

  • There are 3 types of shapes — “square”, “circle” & “rectangle”
  • Now what could a common “key” or the discriminant be in these shapes?
    It could be the “kind” key which can be used as the identifier of the kind of the shape.

With this knowledge, here’s how to implement whatever we have learned so far:

One can create a Discriminated Union type as follows:

type CustomShapeWithDiscriminatedUnion =
| {
kind: "square";
size: number;
}
| {
kind: "rectangle";
width: number;
height: number;
}
| {
kind: "circle";
radius: number;
};

This can be refactored by creating separate types for each shape:

type TSquare = {
kind: "square";
size: number;
};

type TRectangle = {
kind: "rectangle";
width: number;
height: number;
};

type TCircle = {
kind: "circle";
radius: number;
};

type CustomShapeWithDiscriminatedUnion = TSquare | TRectangle | TCircle;

* Is this the only advantage? No, here are a few others:

  1. Easier Refactoring: When refactoring code, discriminated unions make it easier to identify all the places where a particular type is used. This can save time and reduce the risk of introducing errors during refactoring.
  2. Enables Exhaustiveness Checking: TypeScript can check whether all possible types in the union are handled, providing warnings or errors if there are missing cases. This helps prevent unintentional omissions when working with complex data structures.
  3. Facilitates Pattern Matching: Discriminated unions are often used in conjunction with switch statements or conditional checks, enabling pattern matching in TypeScript. This makes it easier to destructure the parameters and handle different cases or variations of a type in a structured and organized manner.
  4. Easier to Understand Code: The discriminant property acts as a clear indicator of the type of object within the union. This makes the code more readable and understandable for developers, especially when dealing with complex data structures
  5. Better Code Completion: IDEs and code editors can provide more accurate code completion suggestions when working with objects that are part of discriminated unions. This is because TypeScript can infer the specific type based on the discriminant property. If you use something like GitHub Copilot, the autocomplete makes writing the code faster and much more enjoyable.

If you carefully read the article till this point and didn’t know about this already, Congratulations! You have learned something new today!!🎉🥳

Here are a few links to learn more about Discriminated Unions in TypeScript:

P.S.:

I am exploring TypeScript and Web Development every day by using it in the projects I work on and through various platforms namely, YouTube (Shorts & long-form content), Instagram Reels, LinkedIn posts, Medium Articles, content on X (formerly, Twitter), peers & seniors, etc., so any comments/feedback/corrections are more than welcome.
I hope you enjoyed this article and learned something new!

Happy coding!

--

--