Using fp-ts and io-ts: types and implementation
Tags: typescript, functional
Published: 2021-03-24 12:00:33

The idea behind [io-ts](https://github.com/gcanti/io-ts/) is to create a value with the type `Type<A, Output, Input>` (also called a "codec") that is the runtime representation of the static type `A`.

In other words, this codec allows to:
- Parse/Deserialize an `Input` and validate that it's an `A` (e.g. parse an `unknown` and validate that it's a `NonEmptyString50`). This part is handled by the `Decoder` side of the codec.
- Serialize an `A` into an `Output` (e.g. serialize an `UnverifiedUser` into a `string`). This part is handled by the `Encoder` side of the codec.

We are going to use only the first part, i.e. the `Decoder`, since we want to take values coming from outside our domain, validate them, then use them inside our business logic.

In this article, I am not going to use the [experimental features](https://github.com/gcanti/io-ts#experimental-modules-version-22). I'll use what is available with the following import as of v2.2.16:

```ts
import * as t from 'io-ts'
```

When decoding an input, the codec returns an `Either<ValidationError[], A>`, which looks very similar to the `Validation<A>` type we wrote in the previous article of this series. Actually, the library exposes a `Validation<A>` type that is an alias to `Either<ValidationError[], A>`.

Previously, we defined the types then we wrote the implementation. Here, we are going to do the opposite: write the implementation, then derive the types from it using the `TypeOf` mapped type provided by `io-ts`.

## First and last names

The equivalent of a "newtype" created with `newtype-ts` is a "branded type" in `io-ts`. We can use the `t.brand` function to create a codec for a branded type:

```ts
interface NonEmptyString50Brand {
  readonly NonEmptyString50: unique symbol
}

const NonEmptyString50 = t.brand(
  t.string,
  (s: string): s is t.Branded<string, NonEmptyString50Brand> => s.length > 0 && s.length <= 50,
  'NonEmptyString50'
)

type NonEmptyString50 = t.TypeOf<typeof NonEmptyString50>
```

First we create the `NonEmptyString50Brand` brand. Next, we create the codec by providing 3 parameters:
- The codec for the "underlying" type of the branded type (here, `string`)
- The type guard function, or "refinement" function
- The name of the codec (optional)

Let's look at the default error message reported for this codec when an invalid input is provided:

```ts
import { PathReporter } from 'io-ts/PathReporter'

PathReporter.report(NonEmptyString50.decode(42))
// ['Invalid value 42 supplied to : NonEmptyString50']
```

If we keep the same logic regarding the errors handling as we did in the previous article, then this message is not particularly "user-friendly". We want a better description of the expected value (string whose size is between 1 and 50 chars). For that, we can use a little helper function provided by [io-ts-types](https://gcanti.github.io/io-ts-types/):

```ts
import { withMessage } from 'io-ts-types'

const FirstName = withMessage(
  NonEmptyString50,
  input => `First name value must be a string (size between 1 and 50 chars), got: ${input}`
)

const LastName = withMessage(
  NonEmptyString50,
  input => `Last name value must be a string (size between 1 and 50 chars), got: ${input}`
)
```

Let's look at the error message reported:

```ts
import { PathReporter } from 'io-ts/PathReporter'

PathReporter.report(FirstName.decode(42))
// ['First name value must be a string (size between 1 and 50 chars), got: 42']
```

We end up with the same error message we had in the previous article, with very little effort thanks to `withMessage`!

## Email address

Nothing fancy here:

```ts
interface EmailAddressBrand {
  readonly EmailAddress: unique symbol
}

// https://stackoverflow.com/a/201378/5202773
const emailPattern = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/i
const EmailAddress = withMessage(
  t.brand(
    t.string,
    (s: string): s is t.Branded<string, EmailAddressBrand> => emailPattern.test(s),
    'EmailAddress'
  ),
  input => `Email address value must be a valid email address, got: ${input}`
)

type EmailAddress = t.TypeOf<typeof EmailAddress>
```

## Middle name initial

We need to create a `Char` codec:

```ts
interface CharBrand {
  readonly Char: unique symbol
}

const Char = t.brand(
  t.string,
  (s: string): s is t.Branded<string, CharBrand> => s.length === 1,
  'Char'
)

type Char = t.TypeOf<typeof Char>
```

Then create a `MiddleNameInitial` codec from it:

```ts
import { optionFromNullable } from 'io-ts-types'

const MiddleNameInitial = withMessage(
  optionFromNullable(Char),
  input => `Middle name initial value must be a single character, got: ${input}`
)
```

This is the same codec as `Char`, but we made it optional with the `optionFromNullable` helper, and we set a custom error message.

## Remaining readings

The `io-ts` library provides a codec for integers, but not for positive integers like we had in `newtype-ts`. We need to create this type:

```ts
interface PositiveIntBrand {
  readonly PositiveInt: unique symbol
}

const PositiveInt = t.brand(
  t.Int,
  (n: t.Int): n is t.Branded<t.Int, PositiveIntBrand> => n >= 0,
  'PositiveInt'
)

type PositiveInt = t.TypeOf<typeof PositiveInt>
```

As you noticed, we can create branded types from other branded types: `t.Branded<t.Int, PositiveIntBrand>`.

Let's define a `RemainingReadings` codec, which is a `PositiveInt` codec with a custom error message:

```ts
const RemainingReadings = withMessage(
  PositiveInt,
  input => `Remaining readings value must be a positive integer, got: ${input}`
)
```

## Verified date

Last but not least, we need a `Timestamp` codec for the verified date:

```ts
interface TimestampBrand {
  readonly Timestamp: unique symbol
}

const Timestamp = t.brand(
  t.Int,
  (t: t.Int): t is t.Branded<t.Int, TimestampBrand> => t >= -8640000000000000 && t <= 8640000000000000,
  'Timestamp'
)

type Timestamp = t.TypeOf<typeof Timestamp>
```

The `VerifiedDate` codec is a `Timestamp` with a custom error message:

```ts
const VerifiedDate = withMessage(
  Timestamp,
  input =>
    `Timestamp value must be a valid timestamp (integer between -8640000000000000 and 8640000000000000), got: ${input}`
)
```

## User types

If you remember from the previous article, we wrote 2 intermediate types before getting a `User`: `UserLike` and `UserLikePartiallyValid`.

To create `UserLike`, we can do the following:

```ts
const UserLike = t.intersection([
  t.type({
    firstName: t.unknown,
    lastName: t.unknown,
    emailAddress: t.unknown
  }),
  t.partial({
    middleNameInitial: t.unknown,
    verifiedDate: t.unknown,
    remainingReadings: t.unknown
  })
])

type UserLike = t.TypeOf<typeof UserLike>
```

The only way to make some properties of an object optional is to make the `intersection` between an object with required properties (`type`) and an object with all the optional properties (`partial`).

Next, we can use some codecs previously defined to build the `UserLikePartiallyValid` codec:

```ts
const UserLikePartiallyValid = t.strict({
  firstName: FirstName,
  lastName: LastName,
  emailAddress: EmailAddress,
  middleNameInitial: MiddleNameInitial
})

type UserLikePartiallyValid = t.TypeOf<typeof UserLikePartiallyValid>
```

I used `strict` here (as opposed to `type`) to make sure any extra property of the input is discarded from a `UserLikePartiallyValid` data object.

Now we can write both `UnverifiedUser` and `VerifiedUser` codecs.

```ts
const UntaggedUnverifiedUser = t.intersection(
  [
    UserLikePartiallyValid,
    t.strict({
      remainingReadings: RemainingReadings
    })
  ],
  'UntaggedUnverifiedUser'
)

type UntaggedUnverifiedUser = t.TypeOf<typeof UntaggedUnverifiedUser>

type UnverifiedUser = UntaggedUnverifiedUser & { readonly type: 'UnverifiedUser' }
```

We first build an `UntaggedUnverifiedUser` because we don't want to include the validation of the `type` property that is used only to create the `User` _sum type_ in TypeScript. Then, we create the `UnverifiedUser` type by adding the `type` property.

Notice that it's only a type definition, there's no codec associated because there's no need to validate external data: we (the developers) are the ones adding the `type` property via the constructor functions (defined a bit later).

We can do the same for the `UntaggedVerifiedUser` codec:

```ts
const UntaggedVerifiedUser = t.intersection(
  [
    UserLikePartiallyValid,
    t.strict({
      verifiedDate: VerifiedDate
    })
  ],
  'UntaggedVerifiedUser'
)

type UntaggedVerifiedUser = t.TypeOf<typeof UntaggedVerifiedUser>

type VerifiedUser = UntaggedVerifiedUser & { readonly type: 'VerifiedUser' }
```

Now that we have both `UnverifiedUser` and `VerifiedUser` types, we can create the `User` type simply with:

```ts
type User = UnverifiedUser | VerifiedUser
```

And the constructor functions:

```ts
const unverifiedUser = (fields: UntaggedUnverifiedUser): User => ({ ...fields, type: 'UnverifiedUser' })

const verifiedUser = (fields: UntaggedVerifiedUser): User => ({ ...fields, type: 'VerifiedUser' })
```

There's one last function we need before (finally) writing the `parseUser` function. We need to detect if a user-like object looks like a verified user or not. In the previous article, we wrote the `detectUserVerification` function. Here, we are going to write a similar function, but instead of taking a `UserLikePartiallyValid` input, it will take a `UserLike` input:

```ts
const detectUserType = <A>({
  onUnverified,
  onVerified
}: {
  onUnverified: (userLikeObject: UserLike) => A
  onVerified: (userLikeObject: UserLike & { verifiedDate: unknown }) => A
}) => ({ verifiedDate, ...rest }: UserLike): A =>
  pipe(
    O.fromNullable(verifiedDate),
    O.fold(
      () => onUnverified(rest),
      verifiedDate => onVerified({ ...rest, verifiedDate })
    )
  )
```

This is because we are going to use the decoders of either `UntaggedUnverifiedUser` or `UntaggedVerifiedUser` codecs that already contain the validation steps for a `UserLikePartiallyValid` object:

```ts
const parseUser: (input: unknown) => t.Validation<User> = flow(
  UserLike.decode,
  E.chain(
    detectUserType({
      onUnverified: flow(UntaggedUnverifiedUser.decode, E.map(unverifiedUser)),
      onVerified:   flow(UntaggedVerifiedUser.decode,   E.map(verifiedUser))
    })
  )
)
```

And that's it! The logic for `parseUser` is slightly different compared to the one we wrote in the previous article, but overall it looks very similar. And, we wrote fewer lines of code for the same result, which is nice (fewer lines = less chances for a bug to be introduced).

The source code is available on the [ruizb/domain-modeling-ts](https://github.com/ruizb/domain-modeling-ts/blob/master/method-2/implementation-and-types.ts) GitHub repository.

---

The `io-ts` library allows us to create codecs that we can combine to build even more complex codecs.

One key difference with the previous method is that the type definition for `User` is not clearly readable for the developers without relying on IntelliSense, and even then, it doesn't show the whole type definition:

```ts
// examples of types displayed with IntelliSense

type UserLikePartiallyValid = t.TypeOf<typeof UserLikePartiallyValid>
/*
type UserLikePartiallyValid = {
  firstName: t.Branded<string, NonEmptyString50Brand>;
  lastName: t.Branded<string, NonEmptyString50Brand>;
  emailAddress: t.Branded<...>;
  middleNameInitial: O.Option<...>;
}
*/

type UnverifiedUser = UntaggedUnverifiedUser & { readonly type: 'UnverifiedUser' }
/*
type UnverifiedUser = {
  firstName: t.Branded<string, NonEmptyString50Brand>;
  lastName: t.Branded<string, NonEmptyString50Brand>;
  emailAddress: t.Branded<...>;
  middleNameInitial: O.Option<...>;
} & {
  ...;
} & {
  ...;
}
*/
```
There is an [open issue](https://github.com/microsoft/vscode/issues/94679) to address this problem in the VS Code editor.

To solve this, we could've first defined the type like we did in the 3rd article of this series, then use `io-ts` for the implementation part only, and not use `TypeOf` to define the types of users.

## Final thoughts

We used these codecs to validate data coming from the external world to use them in our domain. We can **safely** use these data in the functions holding the business logic, at the core of the project. We didn't write these functions in this series though, I chose to focus on the "let valid data enter our domain" part.

If you are familiar with the "onion architecture" (or "ports and adapters architecture") then these codecs take place in the circle wrapping the most-inner one that has the business logic.

This approach allows us to document the code easily by describing and enforcing domain constraints and logic at the type level.

I hope I convinced you to try Domain Driven Design in TypeScript using the `fp-ts` ecosystem!