Using fp-ts and newtype-ts: types
Tags: typescript, functional
Published: 2021-03-24 12:00:17

As a reminder, here's the initial type definition from our domain that we want to improve:

```ts
interface User {
  firstName: string
  lastName: string
  emailAddress: string
  middleNameInitial?: string
  remainingReadings?: number
  verifiedDate?: number
}
```

## What are the constraints?
Here are the constraints that are not reflected in the initial type:
1. Both first and last names must be strings with at least 1 character, and at most 50 characters.
2. The email address must a string containing a... valid email address.
3. The middle name initial is optional: it may not be provided, but if it is, it must be a string containing a single character.
4. The remaining readings must be a positive integer (zero accepted).
5. The verified date must be a timestamp value.

Let's improve the initial type by supporting each constraint, step by step.

In this article, we're only focusing on the type definitions. We will write the constructor functions and validation functions in the next article of this series. These functions will ensure that the types we are defining here are reflected at runtime as well.

#### 1. Both first and last names must be strings with at least 1 character, and at most 50 characters.

We need a type that is valid only when the value provided is a non-empty string that has less than 51 characters.

We are going to use [newtype-ts](https://github.com/gcanti/newtype-ts) to build a _branded type_:

```ts
import { Newtype, Concat } from 'newtype-ts'
import { NonEmptyString } from 'newtype-ts/lib/NonEmptyString'

type String50 = Newtype<{ readonly String50: unique symbol }, string>
type NonEmptyString50 = Concat<String50, NonEmptyString>
```

First, we are declaring a `String50` _branded type_ (or _newtype_) that is the combination of a brand (`{ readonly String50: unique symbol }`) and a primitive type (`string`).

Here, we are _branding_ a `string` in order to get a meaningful subset of strings. The type `String50` **represents** the set of strings that have less than 51 characters.

At this point, the empty string value is still allowed in this set. To exclude it, we can use the `NonEmptyString` _newtype_ provided by `newtype-ts` and "combine" it with our `String50`, thus ending up with the `NonEmptyString50` type.

We can now update our initial "User" definition:

```ts
interface User {
  firstName: NonEmptyString50 // constraints are clear now
  lastName: NonEmptyString50
  emailAddress: string
  middleNameInitial?: string
  remainingReadings?: number
  verifiedDate?: number
}
```

#### 2. The email address must a string containing a... valid email address.

This one is simpler.

```ts
import { Newtype } from 'newtype-ts'

type EmailAddress = Newtype<{ readonly EmailAddress: unique symbol }, string>
```

We could combine it with `NonEmptyString`, but it doesn't bring much value. We know an empty string is not a valid email address, there's no need to support this special case in the type definition.

We can update the `User` type:

```ts
interface User {
  firstName: NonEmptyString50
  lastName: NonEmptyString50
  emailAddress: EmailAddress
  middleNameInitial?: string
  remainingReadings?: number
  verifiedDate?: number
}
```

#### 3. The middle name initial is optional: it may not be provided, but if it is, it must be a string containing a single character.

There are 2 constraints here:
- The value is optional
- If provided, the value must be a single character

There's no native "Character" type in TypeScript (or JavaScript), so we need a _newtype_ for that. Thankfully, `newtype-ts` already provides such type: `Char`.

We could use an [optional property](https://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties) for the `middleNameInitial` field:

```ts
import { Char } from 'newtype-ts/lib/Char'

interface User {
  firstName: NonEmptyString50
  lastName: NonEmptyString50
  emailAddress: EmailAddress
  middleNameInitial?: Char // <- notice the "?"
  remainingReadings?: number
  verifiedDate?: number
}
```

But this means we'll have to make runtime checks to make sure the property is set or not. In the `fp-ts` ecosystem, there's a data type we can use to handle "values that may not exist": `Option`.

```ts
import { Option } from 'fp-ts/Option'

interface User {
  firstName: NonEmptyString50
  lastName: NonEmptyString50
  emailAddress: EmailAddress
  middleNameInitial: Option<Char>
  remainingReadings?: number
  verifiedDate?: number
}
```

Now, the property is required, but we know the value may not exist because we are using the `Option` type.

#### 4. The remaining readings must be a positive integer (zero accepted).

We're almost done with the types! Here, we need a "positive integer" that can be zero. Again, `newtype-ts` already provides a type for us: `PositiveInteger`.

```ts
import { PositiveInteger } from 'newtype-ts/lib/PositiveInteger'

interface User {
  firstName: NonEmptyString50
  lastName: NonEmptyString50
  emailAddress: EmailAddress
  middleNameInitial: Option<Char>
  remainingReadings?: PositiveInteger
  verifiedDate?: number
}
```

We could go even further by defining a subset of `PositiveInteger` that represents the positive integers that are below a certain value. In our example, the maximum value is 3. There are 2 ways to define this subset:

```ts
// combining 2 newtypes, like we did with NonEmptyString50
type RemainingReadings = Concat<
  Newtype<{ readonly NumberBelow3: unique symbol }, number>,
  PositiveInteger
>

// or using a union of literal types,
// since there are not many values allowed
type RemainingReadings = 0 | 1 | 2 | 3
```

Here, I chose to keep `PositiveInteger`, but it's up to you if you want to go further. If you do, I'd recommend using the union of literal types, since it's simpler to write and read/understand.

The challenge when defining these types is to find a good balance to get the "this type carries enough domain information, without going too far" feeling. Using `PositiveInteger` allows us to up the limit to 4 or 5 in the future without changing the code, but it also opens the possibility of having 1000 remaining readings, which could be a bug (or perhaps it's a hidden feature?).

#### 5. The verified date must be a timestamp value.

Last but not least, a timestamp is an integer that is comprised between -8640000000000000 and 8640000000000000.

```ts
import { Newtype, Concat } from 'newtype-ts'
import { Integer } from 'newtype-ts/lib/Integer'

type Timestamp = Concat<
  Newtype<{ readonly TimestampNumber: unique symbol }, number>,
  Integer
>
```

We end up with the following `User` type:

```ts
interface User {
  firstName: NonEmptyString50
  lastName: NonEmptyString50
  emailAddress: EmailAddress
  middleNameInitial: Option<Char>
  remainingReadings?: PositiveInteger
  verifiedDate?: Timestamp
}
```

We can understand the domain constraints by reading this type. We still don't know what's going on with `remainingReadings` and `verifiedDate` though: can they both be defined? Or none of them? What's the domain logic regarding these properties?

## What is the logic?

The domain logic in this case study is the following:
- While users are not verified yet, they are limited in the number of articles they can read.
- Once verified, they can read as many articles as they want.

This means a `User` is either unverified or verified. It cannot be both, or none. Does this ring a bell? That's right, we can use a sum type to model the different types of users in our domain!

```ts
interface UnverifiedUser {
  readonly type: 'UnverifiedUser'
  readonly firstName: NonEmptyString50
  readonly lastName: NonEmptyString50
  readonly emailAddress: EmailAddress
  readonly middleNameInitial: Option<Char>
  readonly remainingReadings: PositiveInteger
}

interface VerifiedUser extends Omit<UnverifiedUser, 'type' | 'remainingReadings'> {
  readonly type: 'VerifiedUser'
  readonly verifiedDate: Timestamp
}

type User = UnverifiedUser | VerifiedUser
```

I made all the properties "read-only" since I think it's a good practice. This forces us to make copies of objects instead of changing the objects directly (immutability at the type level if you will).

Now we know that unverified users are limited in their readings, since the `UnverifiedUser` type has a `remainingReadings` property. We also know that verified users have a verification date (the `verifiedDate` property) and don't have any readings limitation, because `remainingReadings` is missing in `VerifiedUser`.

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

---

So, we've clearly defined our model constraints and logic in the type definition. We should be able to understand what the domain does (or at least get a pretty good idea) only by reading the type. We don't have to look at the code implementation to understand it, which is what we were looking for.

However, we still need some runtime implementation to _actually_ do something. We have to write the code that makes sure the data we receive from outside (the API of a web service) is valid in our domain. This is what we are going to do in the next article!