Bounded types in io-ts
Tags: functional, typescript
Published: 2021-07-11 08:48:00

In the last article, we went through how we created an `Age` type to represent an age between 18 and 100. We also touched upon how in using a stricter type, we are closer to the goal to "make illegal states unrepresentable". However, what happens when we still want to use our stricter types but the data comes over untyped over an API? Furthermore, our `Age` type might only be one small component of larger `User` type so how do we compose our `Age` type into our larger `User` type?

One solution is to use [io-ts](https://gcanti.github.io/io-ts/), a runtime type validation library, also by Giulio Canti. At the core of io-ts there is the idea of a `Codec` of type `Type<A,O,I>` where `A` is our static type, `O` is our output type and `I` is our input type. It also provides codecs for a lot of the [primitives](https://gcanti.github.io/io-ts/modules/Codec.ts.html#primitives) such as string, boolean, number and more.

Codecs have a `decode` method which has the type signature of `I -> Either E A` which loosely translated means that given the input type I, you either get an Error type E or our static type A. For example, if we use the number codec and pass in a string, we will get a Left back representing an error. If we pass in a number however, we will get back a Right back representing a successful decoding.

```
console.log(t.number.decode('3')); // Left error
console.log(t.number.decode(3)); // Right 3
```

However our current problem is that we want a stricter codec than just a number as our `Age` type is only valid for a bounded range of the js number type. Here we rely on io-ts once again through `t.brand` to create a codec for our branded type. Branded types are the io-ts version of new-types also using `unique symbol` underneath the hood.

```
interface AgeBrand {
  readonly Age: unique symbol;
}

const AgeBrandC = t.brand(
  t.number, // codec
  (n: number): n is t.Branded<number, AgeBrand> => n >= 18 && n <= 100, 
// refinement of the number type
  "AgeC" // name of this codec
);
```
Let's unpack this. We first create our branded type `AgeBrand` similar to how we created Age as a new-type in the fp-ts example.

Next t.brand expects a base Codec to build on top of. Since `Age` is a bounded type within numbers, we use the `t.number` codec. As the second argument to `t.brand`, we supply the predicate or refinement function that asks given a number, tell me how to figure out if it is an `Age` type or not. Finally we optionally provide the codec for our branded type a name.

After all this, we now have a codec for our Age type and we can use it to decode things to either get a Left or Right result.
```
console.log(AgeBrandC.decode(13)); //Left error
console.log(AgeBrandC.decode(18)); //Right result
```
But wait, where is our static Age type in this new io-ts world? io-ts provides a handy `Typeof` operator to extract out the static type from a codec.

```
type Age = t.TypeOf<typeof AgeBrandC>;
```

At this point, we have now successfully recovered all of the functionality provided in our fp-ts where instead of `mkAge` we use `decode` to determine whether a number is a valid `Age` or not. However we are yet to tackle the second part of our problem. Namely, what if the Age component is only a larger part of a User component? Here we use the `t.type` function to compose our ageC codec into a larger codec.

```
const UserC = t.type({
  firstName: t.string,
  lastName: t.string,
  age: AgeBrandC
});
```
Here we define our user codec to have three fields, a `firstName`, a `lastName` and an `age`. For the firstName and lastName, we use the primative `t.string` codec to check that they are strings. However for the `age` field, we use our `AgeBrandC` codec to ensure that it is a number between 18 and 100.

However, when trying to decode, it is not very readable to decipher Left or Right results. We can instead use the `PathReporter` error reporter to get a human readable error.

```
console.log(
  PathReporter.report(
    UserC.decode({
      firstName: "foo",
      lastName: "foo",
      age: 13
    })
  )
);
/*["Invalid value undefined supplied to : 
{ firstName: string, lastName: string, age: Age }/lastName: string",
"Invalid value 13 supplied to :
{ firstName: string, lastName: string, age: Age }/age: Age"] */
```

In conclusion, we can use io-ts to strictly type our untyped data at the edge of our application so that within the core business logic, we can use our stricter types and hopefully proceed a little further along our journey to "make illegal states unrepresentable".