Refinement Types in TypeScript — Or how to check that a number must be positive at compile time.

Benoit Lemoine
3 min readApr 3, 2018

All the code in this article has been tested with typescript 2.7.2

Sometimes, the base types offered by TypeScript, like number or string, are not enough to express precisely what you want. For example, you may want to represent the age of a user with a type indicating that it must be a number greater than 0. Or maybe you want to represent an email, which is a string that should match a certain pattern. The answer comes under the form of refinement types, this article aims to show you how to create such types.

What are refinement types?

Refinement types are types defined by two things:

  • a base type;
  • a predicate over this base type to indicate which values are valid. For example, you could have number and (x:number) => x > 0) to represent positive numbers, or string and (s:string) => s.indexOf('_') === 0)) to represent strings starting with _.

This would be easy to model with a wrapper type:

But using a class like this would lead to a lot of wrapping and unwrapping of the value, and this would mean some runtime overhead.

By default, TypeScript doesn’t support refinement types without a runtime wrapper. But there is way to simulate refinement types through type guards.

Reminder on type guards

Type guards are functions that, by doing a runtime checks, guarantee that something is of a given type in some context:

It’s also easy to create custom type guards:

How to refine types?

The type checker trusts the type guard function blindly, even though they are only runtime checks, meaning it’s really easy to write a type guard that lies to the compiler:

This is the property that we will use to simulate refinement types.

Going back to our initial problem, we want to define a type that is “a number which value can only be positive”.

First we will define a type PositiveNumberTag representing a tag indicating that the number IS positive.

We’re using declare here because we don't want this class to exist at runtime. It also means that we will not be able to do new PositiveNumberTag().
We're using a private property named __kind because that way TypeScript will refuse to type checks things like: const a: PositiveNumberTag = {__kind:'positiveNumber'}. The only way to create a PositiveNumberTag will be through the new operator, and as we've seen previously, this will not be possible.

Then we’ll define the type PositiveNumber simply as a number with the previously defined tag:

Now, we’ll need to create a type guard function that will say to the compiler “trust me, what you’re using right now really is a PositiveNumber”, even though this type cannot be built.

How do we use that? Let’s say you want to write a function that needs a timeout, which must be a positive number.

Other examples

RangeValue

This pattern can be used in a variety of contexts beyond positive numbers. We can, for example, extend the previous code to create a numeric type indicating that the number is in a specified range.

Email string

Sometimes you’ll want to check that a string is an email. Refinement types can also be used in this case:

Tagged types

A tagged type is a type composed by an existing type and a tag. Refinement types as defined above are in fact tagged type where the tag is a “proof” that the predicate holds. But we can also rely on the above pattern to create simple tagged types.

This is useful, for example, when manipulating physical quantities like distance or weight. We can model units with tagged types and be sure that we’ll never be able to mix up feet and meters, a mishap known to have led to an infamous satellite crash on Mars.

Conclusion

There are many uses to refinement types; these are surprisingly easy to use in TypeScript, and this without any runtime costs. While Flow has opaque types to simulate an equivalent feature, but this is something that will not land soon (if at all) in TypeScript, so feel free to use refinement types as they are right now!

--

--

Benoit Lemoine

I’m a full-stack developer, in love with functional programming and type systems. I’m working currently at Decathlon Canada, in Montreal QC.