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.
Top comments (0)