Adding type safety, gradually. Part II

4 min read Original article ↗

In the previous post we covered how to get started with babel-plugin-tcomb and flow. In this post we want to introduce a unique feature of tcomb which will be especially interesting for flow users: refinement types.

Refinement types

A refinement type is a type endowed with a predicate which must hold for all instances of the refined type.

That might sound complicated, but really it's very straight forward. Here's an example using vanilla tcomb:

import t from 'tcomb';

const PositiveNumber = t.refinement(
  t.Number,       // <= the type we wish to refine.
  (n) => n >= 0   // <= the predicate that enforces our desired refinement.
);

PositiveNumber(1); // => ok
PositiveNumber(-2); // => throws [tcomb] Invalid value -2 supplied to {Number | <function1>}

There are no limits on what you can do within your predicate declarations. This means you can narrow your types by defining precise invariants, something that static type checkers can do only partially.

Refinements are a very powerful runtime type checking capability of tcomb, but how could we access this power when we are using flow?

Flow

flow can't enforce refinements since they require runtime execution, however when used in combination with babel-plugin-tcomb we can get flow to do our static type checking against the type we are refining whilst also declaring where we would like our runtime refinements to be enforced.

In order for you to define your refinements tcomb exposes the following flow interface:

declare interface $Refinement<P>: Predicate> {}

The $Refinement<P> interface accepts a type parameter P that must be a Predicate (see Bounded Polymorphism for more info). Remember, all predicates need to adhere to the following flow definition:

declare type Predicate = (x: any) => boolean;

Using the $Refinement<P> interface allows you to easily define refinement types. We will explain the usage of this interface via an example.

Let's say that you would like to create a refinement type to enforce that numbers be positive (much like the tcomb example above).

Firstly, you need to define a standalone predicate function that can be used to enforce this rule:

const isPositive = (n) => n >= 0;

A very simple function that takes a number and then ensures the number is greater than or equal to zero.

We can then use this predicate function to define our refinement type by making use of our special $Refinement<P> interface along with a type intersection against the type we are attempting to refine. In this case we are refining the number type.

Here is the complete example on how you would then declare your refinement type:

import type { $Refinement } from 'tcomb';

const PositiveNumber = 
    // The type that we are refining.
    number
    // The intersection operator.
    &
    // The refinement we would like to enforce.
    $Refinement<typeof isPositive>;

We can now use this refinement type as a standard flow type annotation, like so:

function foo(n: PositiveNumber) { }

There are some things you need to note here.

  • flow will do static analysis to ensure that the argument to our foo function is in fact a number.
  • babel-plugin-tcomb interprets our refinement type declaration and ensures that the argument to foo will be checked by our refinement function during runtime.

Let's see what the result would be for various executions of our foo function:

foo(2)    // static checking ok,     runtime checking ok
foo(-2)   // static checking ok,     runtime checking throws "[tcomb] Invalid value -2 supplied to n: PositiveNumber"
foo('a')  // static checking throws, runtime checking throws

Static and runtime type checking are both useful and they are completely complementary: you can get the best of both worlds!

It is also worth noting that your $Refinement<P> declarations are statically type-checked: so if you pass an invalid predicate to $Refinement<P> then Flow will complain:

const double = n => 2 * n; // Invalid! returns a number, not a boolean

type PositiveNumber =
  number &
  $Refinement<typeof double>;

const n: PositiveNumber = 1;

Output:

src/index.js:5
  5: const double = n => 2 * n;
                         ^^^^^ number. This type is incompatible with
  9:   declare interface $Refinement<P: (x: any) => boolean> {}
                                                    ^^^^^^^ boolean. See lib: definitions/tcomb.js:9

Hopefully this post helps to illustrate some of the power in having runtime enforced refinements. With this capability you are able to declare refinement types that could do things like ensure a string is actually a well formed UUID or URL - something that can be tedious to manually test throughout your codebase without having the power of refined types at your fingertips.