DEV Community

loading...

Pragmatic types: exhaustive check

stereobooster profile image stereobooster ・3 min read

It is useful in event processing, or anywhere where you have to deal with pattern matching (like switсh/case in JS) and disjoint unions (aka unions or enums).

Example: assume we have events (or actions in Redux) and we have a function which supposes to process events (or reducer in Redux)

const addTodo = {
  type: 'ADD_TODO',
  payload: 'buy juice'
}
const removeTodo = {
  type: 'REMOVE_TODO',
  payload: 'buy juice'
}
function process(event) {
  switch(event.type) {
    case 'ADD_TODO': // do something
      break;
    case 'REMOVE_TODO': // do something
      break;    
  }
}

Next task is to add one more type of event:

const addTodo = {
  type: 'CHANGE_TODO',
  payload: {
    old: 'buy juice',
    new: 'buy water',
  }
}

We need to keep in mind all the places in the code where we need to change behavior. This is easy if it is two consequent tasks and if there is only one place to change. But what if we need to do it two months later, and you didn't write code in the first place. This sounds harder right. And if this system is in production and change in critical part you will have FUD (Fear-Uncertainty-Doubt).

This is where an exhaustive check shines in.

Flow

function exhaustiveCheck(value: empty) {
  throw new Error(`Unhandled value: ${value}`)
}

type AddTodo = {
  type: 'ADD_TODO',
  payload: string
}
type RemoveTodo = {
  type: 'REMOVE_TODO',
  payload: string
}
type Events = AddTodo | RemoveTodo;

function process(event: Events) {
  switch(event.type) {
    case 'ADD_TODO': // do something
      break;
    case 'REMOVE_TODO': // do something
      break;
    default:
      exhaustiveCheck(event.type);
  }
}

As soon as you add a new event (new value to a union type) type system will notice it and complain.

type ChangeTodo = {
  type: 'CHANGE_TODO',
  payload: string
}
type Events = AddTodo | RemoveTodo | ChangeTodo;

Will result in (try yourself):

26:       exhaustiveCheck(event.type);
                          ^ Cannot call `exhaustiveCheck` with `event.type` bound to `value` because string literal `CHANGE_TODO` [1] is incompatible with empty [2].
References:
14:   type: 'CHANGE_TODO',
            ^ [1]
1: function exhaustiveCheck(value: empty) {
                                   ^ [2]

TypeScript

TypeScript example looks the same except instead of empty use never:

function exhaustiveCheck(value: never) {
  throw new Error(`Unhandled value: ${value}`)
}

The error looks like:

Argument of type '"CHANGE_TODO"' is not assignable to parameter of type 'never'.

This post is part of the series. Follow me on twitter and github.

Discussion (0)

pic
Editor guide