How to Simplify Your Code with TypeScript Discriminated Union Types
Tags: typescript, redux, react, webdev
Published: 2024-05-26 07:40:45

## 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
    
    ```json
    // 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:
    
    ```json
    {
    	"type": 'Circle',
    	"radius": 50
    }
    
    {
    	"type": 'Square',
    	"sideLength": 10
    }
    ```
    

Now their interfaces will look something like below:

{% embed https://codesandbox.io/embed/g7j84t?view=editor&module=%2Findex.ts %}


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

{% embed https://codesandbox.io/embed/vgx3kz?view=editor&module=%2Findex.ts %}

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:

{% embed https://codesandbox.io/embed/yg9sls?view=editor&module=%2Findex.ts %}

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

{% embed https://codesandbox.io/embed/vljmcs?view=editor&module=%2Findex.ts %}

## 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:

{% embed https://codesandbox.io/embed/353862?view=editor&module=%2Fsrc%2Fstore%2Factions.ts %}

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:

```tsx
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,
    },
  });
```

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:

```tsx
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;
    }
  }
};
```

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

```tsx
const payload: {
    id: number;
    text: string;
} | {
    id: number;
}
```

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:

{% embed https://codesandbox.io/embed/353862?view=editor&module=%2Fsrc%2Fstore%2FactionCreators.ts %}

`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:

{% embed https://codesandbox.io/embed/353862?view=editor&module=%2Fsrc%2Fstore%2Freducers.ts %}

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 [twitter](https://twitter.com/keurplkar), [github](http://github.com/keyurparalkar), and [linkedIn](https://www.linkedin.com/in/keyur-paralkar-494415107/).