Discriminated unions in ReactJS

Mohammad Jawad (Kasir) Barati - Oct 18 - - Dev Community

Hey folks,

I am pretty excited to share this with you all. It is about how you can have better typing and intelisense in your ReactJS app. So if you wanna utilize TS features to the maximum sit tight.

Problem

So I was reading ReactJS doc and I saw this example So I decided to implement it in ReactJS + CSS + Typescript. No third party lib or CSS framework. And the result is super duper beautiful if you ask me.

So here is the catch about how I wanted to use useReducer hook:

  • I had my reducer defined like this:
  function reducer(tasks: Task[], action: TaskManagerAction): Task[] {
    if (action.type === 'create') {
      return [
        ...tasks,
        {
          id: Math.random().toString(),
          name: action.taskName,
          inProgress: false,
        },
      ];
    }
    if (action.type === 'update') {
      return tasks.map((task) => {
        if (task.id === action.taskId) {
          return {
            id: task.id,
            name: action.taskName,
            inProgress: action.taskInProgress,
          };
        }
        return task;
      });
    }
    if (action.type === 'delete') {
      return tasks.filter((task) => task.id !== action.taskId);
    }

    throw 'invalid action type!';
  }
Enter fullscreen mode Exit fullscreen mode
  • And firstly I wrote my TaskManagerAction like this:
  interface TaskManagerAction {
    type: 'update' | 'create' | 'delete';
    taskId: string;
    taskName: string;
    taskInProgress: boolean;
  }
Enter fullscreen mode Exit fullscreen mode
  • Then I tried to use it as my reducer function in useReducer:
  const [tasks, dispatch] = useReducer(reducer, []);
Enter fullscreen mode Exit fullscreen mode
  • But soon I've realized that when I am gonna use dispatch method to invoke one of those actions I have to either make all three other keys optional or pass them. It does not matter that I did not need them, but I had to because they were defined as required.

And making them all optional was not a good choice either because it defeats the purpose of using TS.

Solution

Then I decided to poke around an realized that TS has this awesome feature where it can narrow down the type based on the action.type that we're passing to the dispatch method.

So it is a life saver, let's look at it:

interface CreateTaskManagerAction {
  type: 'create';
  taskName: string;
}
interface UpdateTaskManagerAction {
  type: 'update';
  taskId: string;
  taskName: string;
  taskInProgress: boolean;
}
interface DeleteTaskManagerAction {
  type: 'delete';
  taskId: string;
}
type TaskManagerAction =
  | CreateTaskManagerAction
  | UpdateTaskManagerAction
  | DeleteTaskManagerAction;
Enter fullscreen mode Exit fullscreen mode

As you can see I am telling TS that TaskManagerAction is either one of those three other types and they have all different key-value pairs. and inside the code you can see that VSCode is deriving the type of action from action.type:

VSCode screenshot of how TS can derive the type from  raw `action.type` endraw

And You can find my code here. Go to src/components/task-manager:


I also found this YouTube video in which he is explaining another use case of discriminated unions in ReactJS:

. . . . . . . . . . . . . . . . .
Terabox Video Player