Exhaustive switch in Flow

Exhaustive switch in Flow

By Guillaume Claret

The static type checker FlowType supports the algebraic data types by encoding sums with disjoint unions of objects. However, Flow does not check if a switch over a disjoint union is exhaustive. We present a trick to detect at compile-time these non-exhaustive switch. Thus we can make sure that we do not miss any cases before putting our code into production.

The Gist

Our trick relies on a special Empty type:

type Empty = 'empty type' & 'nothing there';

function unexpectedCase(impossible: Empty): void {
  reportError(`Unexpected case ${impossible}`);
}

type t = 'a' | 'b';

// This function will type-check if and only if we handle all the cases.
function toNumber(value: t): number {
  switch (value) {
    case 'a':
      return 0;
    case 'b':
      return 1;
    default:
      unexpectedCase(value);
      return 0;
  }
}

You can try this example on TryFlow. If you add a third case:

type t = 'a' | 'b' | 'c';

you will get the following error:

string literal `c`
Expected string literal `empty type`, got `c` instead
string literal `empty type`

How does this work?

For each case of a switch, Flow refines the type of the value according to the current case. For example, in:

case 'a':
  return 0;

the value is known to be the string 'a'. In a default case, the type of value is refined to the remaining cases or to any if the switch is exhaustive. One approach to handle this impossible value of type any is to raise an exception:

default:
  throw new Error('Impossible case');

This is dangerous, because if our Flow program is not 100% typed we could still run into the default branch and get an uncaught exception into production. Instead, we report the error and return a default value:

    default:
      unexpectedCase(value);
      return 0;

// ...

function unexpectedCase(impossible): void {
  reportError(`Unexpected case ${impossible}`);
}

Our aim is to generate a Flow error if and only if the switch is not exhaustive, that is to say if and only if the parameter impossible of the function unexpectedCase has type any. We achive that by introducing a type which has no elements:

type Empty = 'empty type' & 'nothing there';
// We coud do as well: type Empty = bool & string;

This type is empty because there are no strings which are both equal to 'empty type' and to 'nothing there'. Thus, the only way to obtain a value of type Empty is to cast a value of type any, which is exactly what we are looking for. Hence the function:

function unexpectedCase(impossible: Empty): void {
  reportError(`Unexpected case ${impossible}`);
}

will do the trick to force the switch to be exhaustive.

Related

In most functional languages, destructuring a sum type without being exhaustive raises an error or a warning. In Flow, Adam Solove gives an alternative trick to solve the exhaustiveness problem of the switch. However, this alternative technique does not work in some conditions (if the return type of the function is string) and does not return a safe default value. In our experience, it is important to return a valid default value because most Flow programs rely on untyped third-party libraries. Thus it happended to us to go through supposedly impossible default in production, so it is better to get a log report than to risk a runtime error.

OuiCar's Picture

About OuiCar

OuiCar is a community market place to find and rent cars anywhere in France. But we are not just car renters. We also like to experiment new technologies and share ideas with others.

Paris http://www.ouicar.fr

Comments