DEV Community

Cover image for How to Simplify Your Code with TypeScript Discriminated Union Types
Keyur Paralkar
Keyur Paralkar

Posted on

How to Simplify Your Code with TypeScript Discriminated Union Types

Introduction

This will be my first time writing about TS. There are multiple tutorials out there which explain the concept of discriminated unions in an outstanding way. But I will I try my best to explain here my understanding about the concept and how I approached it.

In this blogpost, we are going to learn about discriminated union types. We will also look at how it is an excellent concept that makes developer’s life less miserable. You will also get to see some realtime examples.

What are discriminated Union Types?

Let me be a good dev and explain this concept by explaining the problem and then the solution.

Problem Statement

An interface or a type can represent multiple states. And based on these states the other properties in the interface can differ. For example,

  • A network response has the following structure

    // For successful request
    {
        "status": '200',
        "payload": {...}
    }
    
    // For failure in request
    {
        "status": '400',
        "error": {...}
    }
    

    As you can see, based on the status property the rest/some of the properties might be present or may note be present.

  • A general shape interface can have multiple properties that might not be present in all the shapes:

    {
        "type": 'Circle',
        "radius": 50
    }
    
    {
        "type": 'Square',
        "sideLength": 10
    }
    

Now their interfaces will look something like below:

Now suppose for the Response interface we want to write a utility function that logs the response in different format like below:

Both of these blocks will have access to the successPayload and errorPayload i.e. we know that type 200 means a successful request and we want only to get successPayload and nothing else. Similar is the case for the type 400

The mental model for the types we have is a mapping of status state i.e. type value with the rest of the parameters.

As you can see this is a very common problem that all the developers face. To resolve this and tell typescript to distinguish between different values of types we need to make use of Discriminated union types of typescript.

Discriminated Union Types

This is a nice little feature that Typescript provides that helps the TS itself to distinguish between which rest of the parameters to choose based on a common property between them. Let us restructure our above example of NetworkResponse interface:

Now if we try use the displayNetworkResponse function we won’t get any type errors.

Realtime Use case

I have always faced this issue while using the React’s Context APIs where I have my actions ready with me and each and every action would have a different payload property. But when I am building my reducer function I don’t get the narrowed typing based on the action case I am in inside the switch case. Let me demonstrate this with an example.

Suppose you have these set of actions with you:

These are simple actions like adding and removing a TODO from a list

Next, we define the action creators or the functions that dispatch the action like below:

import { ACTIONS } from "./actions";

export const addTodo = (id: number, text: string) =>
  ({
    type: ACTIONS.ADD_TODO,
    payload: {
      id,
      text,
    },
  });

export const removeTodo = (id: number) =>
  ({
    type: ACTIONS.REMOVE_TODO,
    payload: {
      id,
    },
  });
Enter fullscreen mode Exit fullscreen mode

As we see here the payload attribute of both the actions is different. For ADD_TODO action the payload attribute consists of id and text and for REMOVE_TODO action we just pass an id attribute.

Now let us define our reducer function like below:

import { ACTIONS } from "./actions";

export const reducer = (state, action) => {
  switch (action.type) {
    case ACTIONS.ADD_TODO: {
      const { payload } = action;

      payload.text;
    }

    case ACTIONS.REMOVE_TODO: {
      const { payload } = action;

      payload.id;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

In both the action cases TS is not able to decide the types for the payload. It will display typings as follows:

const payload: {
    id: number;
    text: string;
} | {
    id: number;
}
Enter fullscreen mode Exit fullscreen mode

But this is not what we want. We expect that when the ADD_TODO action is being used as the payload attribute then it should consists of id and text attribute and for REMOVE_TODO we expect that it should consists of only the id attribute. How can typescript know this mapping? Discriminated Union Types to the rescue.

We need to return the ActionTypes type from the actions file like below:

ActionTypes here is a type that will be a union of return type of addTodo and removeTodo function. Here the common property to discriminated will be the type property in both function’s return type. Now making use of this type inside the reducer function like below, helps us to achieve what we wanted:

Now if you check the payload attribute in each section then we are able to see that payload types are getting changed based on the action type.

So this is the magic of Discriminated Union Types.

Summary

I feel discriminated union type is the most underrated feature for typescript. I believe every developer should use it to have better type narrowing that will help to write better and predictable code.

Thanks a lot for reading my blogpost.

You can follow me on twittergithub, and linkedIn.

Top comments (0)