Basic Types are a Red Flag, Part 1
Tags: typescript, functional
Published: 2021-01-04 18:33:39

---
title: Basic Types are a Red Flag, Part 1
published: true
description: Explore more type-safe ways of designing complex-types
tags: typescript, functional
---

What if I told you that every time I see `string`, `number`, `bool`, `[]`, etc I consider it a red flag? You'd probably think I was crazy. After all, those are the building blocks by which we create our types! They are like the atoms that make up the real world.

This is true, but we can - and probably should - be more sparing in how we use these types. Let's look at an example:

```typescript
type Customer = {
  id: number;
  firstName: string;
  lastName: string;
  loyaltyCard: string | null;
};
```

Right, so we have a simple Customer, and on the surface it looks like it has all the info we need, but by designing our types like this we break an important rule of software design: never let our application get into an invalid state.

Let's ask ourselves a few question:

1. Can `firstName` or `lastName` be empty strings? _No_
2. Does the loyalty card have a specific format? _Let's say it is a 12-digit number_

If you come from an object oriented background, like myself, you may be thinking, "This is easy! Make it a class and use a constructor!" Ok, let's do that!

```typescript
class Customer {
  firstName: string;
  lastName: string;
  loyaltyCard: number;

  constructor(firstName: string, lastName: string, loyaltyCard: number) {
    if (firstName.trim() === "") throw new Error("firstName cannot be empty!");
    if (lastName.trim() === "") throw new Error("lastName cannot be empty!");
    if (loyaltyCard.toString().length != 9)
      throw new Error("loyaltyCard must be 9 digits!"); // Ignoring checking for non-numeric chars for the moment

    this.firstName = firstName;
    this.lastName = lastName;
    this.loyaltyCard = loyaltyCard;
  }
}
```

This is definitely a step in the right direction. By gathering our required parameters in a constructor we ensure the object will be properly hydrated, and we can do all of our validation before we allow our class to be instantiated. There are still a few issues with this approach, however. Firstly, because we are throwing exceptions we won't know if any code tries to instantiate a Customer with invalid values until runtime. This is because most popular OO languages don't typically give us a way to encode preconditions and their exceptions into the language, therefore we can't get compile-time checks around them[^1].

So, how do we do this?

## Give your Types Self-Documenting Names

Let's go back to our original types, and encode some more specific types

```typescript
type NonEmptyString = string;
type StringLength12 = string;

type Customer = {
  id: number;
  firstName: NonEmptyString;
  lastName: NonEmptyString;
  loyaltyCard: StringLength12 | null;
};
```

Ok, this approach looks good! Now our types have some information about their required values. But they are really just type aliases over the types we had before. So, compared to the purely OO approach we had before we've taken one step forward but two steps back. Is there a way to add a bit more safety around these new types?

## Introducing Smart Constructors

Smart Constructors allow us to guarantee our types fulfill special requirements in order to construct them while also providing valuable compile-time checks. Let's introduce some smart constructors. To do this I need to use a popular type in functional programming called `Either<L,R>`[^2]. I am going to use the `Either` from one of my favorite libraries `fp-ts`[^3]. Let's add some smart constructors:

```typescript
export const isNonEmptyString = (s: string): s is NonEmptyString =>
  s.trim() !== "";

export const NonEmptyString = E.fromPredicate(
  isNonEmptyString,
  (s) => "String cannot be empty"
);

export const isStringWithLength = (length: number) => (
  s: string
): s is StringWithLength => s.length === length;

export const StringWithLength = (length: number) =>
  E.fromPredicate(
    isStringWithLength(length),
    (s) => `'${s}' is not ${length} characters`
  );
```

Now, when we try to create `Customer`:

```typescript
const customer: Customer = {
  id: 12,
  firstName: "",
  lastName: "",
  loyaltyCard: 2,
};
```

We get errors: ![Incorrect Customer Creation](https://dev-to-uploads.s3.amazonaws.com/i/xtdtrx3vwfkfdqvmzd3c.png)

But now I have a pile of Eithers.

```Typescript
const firstName = NonEmptyString("John");
const lastName = NonEmptyString("Doe");
const loyaltyCard = StringWithLength(12)("0123456789123456");
```

How do I get the values to create a `Customer` and do something with it? Well, we can use something called `sequence`. `Sequence` allows us to convert an `array`, `tuple` or `struct` of `Either`, or any other functor, to one `Either` of all our values if they are all successful.

Let's start by putting all our values into an object:

```typescript
const firstName = NonEmptyString("John");
const lastName = NonEmptyString("Doe");
const loyaltyCard = StringWithLength(12)("0123456789123456");

const pileOfEithers = { firstName, lastName, loyaltyCard };
```

Here you can see what the object looks like:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nuyemerw021vxxkiyxe6.png)

Let's use `sequenceS` (the 'S' stands for struct. There is also `sequenceT` for tuples and `sequenceA` for arrays) to invert our type:

```typescript
const seq = sequenceS(E.either);
const sequencedEithers = seq(pileOfEithers);
```

After we have used `sequenceS`
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ru7mfogmzj9xwrds6d9h.png)

Now let's look at the whole thing and how we can chain our sequenced values into some dispatch functions.

```typescript
import * as E from "fp-ts/Either";
import { sequenceS } from "fp-ts/lib/Apply";
import { pipe } from "fp-ts/lib/function";

// Simulate some dispatch functions to deal with our results
declare function dispatchError(error: string);
declare function dispatchCustomer(customer: Customer);

const firstName = NonEmptyString("John");
const lastName = NonEmptyString("Doe");
const loyaltyCard = StringWithLength(12)("0123456789123456");

const pileOfEithers = { firstName, lastName, loyaltyCard };

const seq = sequenceS(E.either);
const sequencedEithers = seq(pileOfEithers)

const Customer = (c) => ({
  id: 12,
  firstName,
  lastName,
  loyaltyCard,
});

pipe(
  sequencedEithers
  E.map((c) => ({ ...c, id: 12 })),
  E.fold(dispatchError, dispatchCustomer)
);
```

The `E.fold` on the last line is like reduce. It aggregates multiple values down to a single one. In our case we are converting the `Either<string, Customer>` to the single type `void` because our two dispatch function do not return a value. If the function did not return void they would still need to return the same type.

In a language like F# we would use 'Pattern Matching', like this:

```Fsharp
  match customerEither with
  | Customer c -> dispatchCustomer(c)
  | string error -> dispatchError(error)
```

That's it for now. In the next part of the series we will use our other types and look at how we can refine them even more!

## Sources

1. [Domain Modeling Made Functional by Scott Wlaschin](https://pragprog.com/titles/swdddf/domain-modeling-made-functional/)
2. [F# For Fun and Profit](https://fsharpforfunandprofit.com/)
3. [Getting started with fp-ts Series by fp-ts Creator Giulio Canti](https://dev.to/gcanti/series/680)

[^1]: C# tried this with Code Contracts and Java with force exception handling, but both were a bit clunky to use, because often times we don't want to deal with errors until we are in the correct context.
[^2]: Here is some information on [Either](https://dev.to/gcanti/getting-started-with-fp-ts-either-vs-validation-5eja)
[^3]: fp-ts documentation can be found [here](https://gcanti.github.io/fp-ts/)