Pragmatic types: exhaustive check

stereobooster - Aug 21 '18 - - Dev Community

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;    
  }
}
Enter fullscreen mode Exit fullscreen mode

Next task is to add one more type of event:

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

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

TypeScript

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

function exhaustiveCheck(value: never) {
  throw new Error(`Unhandled value: ${value}`)
}
Enter fullscreen mode Exit fullscreen mode

The error looks like:

Argument of type '"CHANGE_TODO"' is not assignable to parameter of type 'never'.
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .