Notes on Advanced TypeScript: Runtime Validations
Tags: react, typescript, types
Published: 2022-05-04 14:24:31

---
title: Notes on Advanced TypeScript: Runtime Validations
published: true
description: Notes on Advanced TypeScript
tags: react, typescript, types
series: Notes on Advanced TypeScript
---

### Introduction

These notes should help in better understanding advanced `TypeScript` topics and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples are based on TypeScript 4.6.

**Note**: This post is an update version of the original [Notes on TypeScript: Handling Side-Effects](https://dev.to/busypeoples/notes-on-typescript-handling-side-effects-3nid)

### Basic

There are situations when working with TypeScript, where we can't guarantee that the types reflect the actual data we are working with. Examples for these types of situations include reading from a file, fetching data from an external endpoint or loading information saved in local storage. In all of the above scenarios we can't guarantee that the data entering our application actually reflects the types we defined. Further more, in any of these scenarios we can be running into runtime errors, no matter what the type actually claims.
This means once we're dealing with external data, that is not defined at compile time, we need some mechanism to safely handle this type of data.

To make it more practical, let's assume the following situation: we want to load a `user` from a pre-defined endpoint.

```ts
const loadUser = (id: number) => {
  fetch(`http://www.your-defined-endpoint.com/users/${id}`)
    .then((response) => response.json())
    .then((user: User) => saveUser(user))
    .catch((error) => {
      console.log({ error });
    });
};
```

At first look this all sounds reasonable, we fetch a user by id, and then save the data for further processing. If you take a closer look at the code, you will notice that we defined the data to be of type `User` after decoding the json data. The `User` type in this example is defined as follows:

```ts
type User = {
  id: number;
  name: string;
  active: boolean;
  profile: {
    activatedAt: number;
  };
};
```

Interestingly the code will compile and TypeScript will show no errors as we defined a `User` and claimed that the response, once decoded, will always be of aforementioned type. Even more interesting is the fact that calling the **json** function on the response object returns an **`Promise<any>`**, so there is no actual guarantee that we are dealing with a `User` type at runtime.

Let's see a scenario where our assumptions might fail, so let's add a `saveUser` function, that expects a user with some profile information:

```ts
const saveUser = (user: User) => {
  const activationDate = user.profile.activatedAt;
  // do something with the information...
};
```

Now how can our application break? The code above will compile, but what happens when the returned _user_ object doesn't have any profile information? Let's assume that at runtime, we suddenly receive the following object:

```ts
{
  id: 1,
  name: "Some User Name",
  active: true,
  extended: {
      activatedAt: 1640995200000
  }
};
```

The result will still be a `User` inside our application, but we will run into an error at runtime, as soon as we call the `saveUser` function. One way to deal with this, is to get more defensive, by exteding our function to check if the property `profile` even exists:

```ts
const saveUser = (user: User) => {
  if (user && user.profile && user.profile.activatedAt) {
    const activationDate = user.profile.activatedAt;
    // do something with the information...
  } else {
    // do something else
  }
};
```

But this will quickly become complicated when we have to do these checks all over our application when working with external data. Rather, we want to do this check as early as possible, in fact at the moment we have access to said data.

### Advanced

TypeScript doesn't offer any runtime **JSON** validation capabilities, but there are libraries in the TypeScript eco-system that we can leverage for that specific case.
We will use the popular `io-ts` library to ensure the data we are working on is reliable throught the application. Our approach will be to decode any external data entering our application.

[`io-ts`](https://github.com/gcanti/io-ts) is written by [Giulio Canti](https://twitter.com/GiulioCanti) and offers runtime type validations. For more information on io-ts consult the [`README`](https://github.com/gcanti/io-ts). So called `codecs` are used to encode/decode data.These codecs are runtime representations of specific static types and can be composed to build even larger type validations.

Codecs enable us to encode and decode any in/out data and the built-in `decode` method returns an `Either` type, which represents **success** (Right) and **failure** (Left). Via leveraging this functionality we can decode external data and handle the success/failure case specifically. To get a better understanding let's rebuild our previous example using the `io-ts` library.

```js
import * as t from "io-ts";

const User = t.type({
  id: t.number,
  name: t.string,
  active: t.boolean,
  profile: t.type({
    activatedAt: t.number,
  }),
});
```

By combing different codecs like `string` or `number` we can construct a `User` runtime type, that we can use for validating any incoming `user` data.

The previous basic construct has the same shape as the `User` type we defined previously. What we don't want though, is to redefine the `User` as a static type as well. `io-ts` can help us here, by offering `TypeOf` which enables user land to generate a static representation of the constructed `User`.

```js
type UserType = t.TypeOf<typeof User>;
```

Interestingly this will give us the same representation we defined in the beginning:

```js
type UserType = {
  id: number,
  name: string,
  active: boolean,
  profile: {
    activatedAt: number,
  },
};
```

Once we have a defined shape, we can verify if the data is of that expected shape and either handle the success or failure case:

```ts
const userA = {
  id: 1,
  name: "Test User A",
  active: true,
  profile: {
    activatedAt: t.number,
  },
};

const result = User.decode(userA);

if (result._tag === "Right") {
  // handle the success case
  // access the data
  result.right;
} else {
  // handle the failure
}
```

The result of the _decode_ function contains a `_tag` property that can either be a `Right` or `Left` string, which represent success or failure. Furthermore we have access to a `right` and `left` property, containing the decoded data in the success case (right) or an error message in the failure case (right).
The above example can be extended to use a so called `PathReporter` for error message handling:

```ts
import { PathReporter } from "io-ts/lib/PathReporter";

if (result._tag === "Right") {
  // handle the success case
  // access the data
  result.right;
} else {
  // handle the failure
  console.warn(PathReporter.report(result).join("\n"));
}
```

`io-ts` also comes with `fp-ts` as a peer dependency, which offers useful utility functions like `isRight` or `fold`. We can use the the `isRight` function to check if the decoded result is valid, instead of having to manually handle this via the `_tag` property.

```js
import * as t from "io-ts";
import { isRight } from "fp-ts/lib/Either";

const userA = {
  id: 1,
  name: "Test User A",
  active: true,
  profile: {
    activatedAt: t.number,
  },
};

isRight(User.decode(userA)); // true

const userB = {
  id: 1,
  name: "Test User",
  active: true,
  extended: {
    activatedAt: t.number,
  },
};

isRight(User.decode(userB)); // false
```

One more useful functionality that will help us when working with the `Either` type, that the decode returns is `fold`, which enables us to define a success and failure path, check the following example for more clarification:

```js
const validate = fold(
  (error) => console.log({ error }),
  (result) => console.log({ result })
);

// success case
validate(User.decode(userA));

// failure case
validate(User.decode(userB));
```

Using `fold` enables us to handle valid or invalid data when calling our fetch functionality. The `loadUser` function could now be refactored to handle these cases.

```js
const resolveUser = fold(
  (errors: t.Errors) => {
    throw new Error(`${errors.length} errors found!`);
  },
  (user: User) => saveUser(user)
);

const loadUser = (id: number) => {
  fetch(`http://www.your-defined-endpoint.com/users/${id}`)
    .then((response) => response.json())
    .then((user) => resolveUser(User.decode(user)))
    .catch((error) => {
      console.log({ error });
    });
};
```

We might handle any incorrect representation by throwing another error. This prevents the data from being passed around in our application. There are more improvement we can make here. Right now, we're being very specific in how we're handling the `User` decoding. There might be an opportunity to write a general function that handles any promise based data.

```js
const decodePromise = <I, O>(type: t.Decoder<I, O>, value: I): Promise<O> => {
  return (
    fold < t.Errors,
    O,
    Promise <
      O >>
        ((errors) => Promise.reject(errors),
        (result) => Promise.resolve(result))(type.decode(value))
  );
};
```

Our `decodePromise` function handles any input data based on a defined decoder and then returns a promise, based on running the actual decoding operation.

```js
const loadUser = (id: number) => {
  fetch(`http://www.your-defined-endpoint.com/users/${id}`)
    .then((response) => response.json())
    .then((user) => decodePromise(User, user))
    .then((user: User) => state.saveUser(user))
    .catch((error) => {
      console.log({ error });
    });
};
```

There are more improvements we could make, but we should have a basic understanding of why it might be useful to validate any external data at runtime. `io-ts` offers more features handling recursive and optional types. Furthermore there are libraries like [`io-ts-promise`](https://github.com/aeirola/io-ts-promise) that provide more features and useful helpers, the above `decodePromise`, for example, is available in a more advanced variant via `io-ts-promise`.

---

### Links

[io-ts](https://github.com/gcanti/io-ts)

[io-ts-promise](https://github.com/aeirola/io-ts-promise)

---

If you have any questions or feedback please leave a comment here or connect via Twitter: [A. Sharif](https://twitter.com/sharifsbeat)