Notes on TypeScript: Handling Side-Effects
Tags: typescript, types
Published: 2020-01-28 17:04:49

---
title: Notes on TypeScript: Handling Side-Effects
published: true
description: Handling Side-Effects
tags: typescript, types
series: Notes on TypeScript
---

### Introduction

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

---

### Basics

When working with TypeScript, we want to rely on the defined types throughout the application.
But can we always rely on these types? There are cases where we can't guarantee that these types are valid at compile time. Let's think about some possible cases where this might apply. 
When working with external files that we might load at runtime, or a simple __API fetch__. We can't guarantee that what we expect is actually the expected type at runtime. There might be cases where we control the complete application cycle and can assume that the expected types apply, but even then, we're dealing with an side-effects, that we want to handle safely as possible.

Let's see an example where we assume a specific type is actually valid, but where we might run into problems at runtime.

```js
type Theme = "basic" | "advanced";

type User = {
  id: number;
  name: string;
  points: number;
  active: boolean;
  settings: {
    configuration: {
      theme: Theme;
    };
  };
};
```

`User` type defines a specific user shape, nothing too special so far. In our application we will probably be retrieving this user object via an endpoint and use this user object to later update some properties.

Somewhere in our app, we want to enable to change the user settings, to make the profile configurable. `changeTheme` is a simplified function that accepts a possible theme and updates the user settings.

```js
const changeTheme = (user: User, theme: Theme): User => {
  return {
    ...user,
    settings: {
      ...user.settings,
      configuration: { ...user.settings.configuration, theme }
    }
  };
};
```

The `user` object has to come from somewhere, so let's assume we have a function that takes care of loading that user. 


```js
const loadUser = () => {
  fetch("http://localhost:3000/users/1")
    .then(response => response.json())
    .then((user: User) => state.saveUser(user))
    .catch(error => {
      console.log({ error });
    });
};
```

Furthermore we might want to keep state of that user inside the client, so we call the `saveUser` function to keep track of the object.


```js
type State = {
  user: User | undefined;
  saveUser: (user: User) => void;
  getUser: () => User | undefined;
};

const state: State = {
  user: undefined,
  saveUser: (user: User) => {
    state.user = user;
  },
  getUser: () => {
    return state.user;
  }
};
``` 

TypeScript will not complain. Calling the `json` function on the response object returns an `Promise<any>`, so there is no guarantee that we are dealing with a `User` type. The following code will compile as well. If you recall `changeTheme` expects a `User` type.

```js
const user = state.getUser();
if (user) {
  const result = changeTheme(user, "advanced");
}
```

When calling `changeTheme` with the following fetch result, we will be greeted with an error at runtime. We expected the property to have the `settings` key, but it actually returned `setting`.

```js

const user = {
  id: 1,
  name: "Test User",
  points: 100,
  active: true,
  setting: {
    configuration: {
      theme: "basic"
    }
  }
};

changeTheme(user, "advanced");

// Error! 
// Uncaught TypeError: Cannot read property 'configuration' of undefined at changeTheme

```

This example might be very simplified, but it should show that we can't fully rely on the data coming from an external source. We could be more defensive when working with `user`, which we should be doing anyway, but it also means that we can rely on these types even less. Adding a check will prevent the previous error.

```js
if (!user || !user.settings || !user.settings.configuration) {
  return user;
}
```

We have seen that we need to be more restrictive when working with external data and relying TypeScript defined types, but there might be a more convenient way to handle side-effects when working inside a TypeScript codebase.

---


### Advanced

If you have worked with `Elm` or `ReasonMl` before, you might have noticed that there is a way to encode and decode any JSON data. This is very valuable as we want to ensure that JSON data we are getting has the expected type. TypeScript by default doesn't offer any runtime JSON validators, but there are a couple of possible solutions to solving that problem in TS land. We will use one of these libraries to show how we can make our types more reliable.

[`io-ts`](https://github.com/gcanti/io-ts) is a library written by [Giulio Canti](https://twitter.com/GiulioCanti) that is focused on runtime type validations. We will not get too deep into the details and capabilities of this library, as the official [`README`](https://github.com/gcanti/io-ts) covers most of the basics. But it's interesting to note, that the library offers a large range of so called `codecs`, which are runtime representations of specific static types. These codecs can then be composed to build larger type validations.

Codecs enable us to encode and decode in and out data and the built-in `decode` method that every code exposes, returns an `Either` type, which represents success (Right) and failure (Left). This enables us to decode any external data and handle success/failure 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,
  points: t.number,
  active: t.boolean,
  settings: t.type({
    configuration: t.type({
      theme: t.keyof({
        basic: null,
        advanced: null
      })
    })
  })
});

```

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>;
```

This gives us the exact same representation we defined in the beginning of this post.

```js
type UserType = {
  id: number;
  name: string;
  points: number;
  active: boolean;
  settings: {
    configuration: {
      theme: 'basic' | 'advanced';
    };
  };
};
```

To clarify the concept, let assume we have a `user` object and want to validate if it has the expected shape. `io-ts` also comes with `fp-ts` as a peer dependency, which offers useful utility functions like `isRight` or `fold`. We can use the `isRight` function to check if the decoded result is valid.

```js
const userA = {
  id: 1,
  name: "Test User",
  points: 100,
  active: true,
  settings: {
    configuration: {
      theme: "basic"
    }
  }
};

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

const userB = {
  id: 1,
  name: "Test User",
  points: "100",
  active: true,
  settings: {
    configuration: {
      theme: "basic"
    }
  }
};

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


Now that we have some basic understanding of how `io-ts` functions, let's revisit the original `loadUser` function and decode the data before we pass it around inside our application. But before we start refactoring let's take a look at one more useful functionality, that will help us when working with the `Either` type, that the decode returns. `fold` 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) => state.saveUser(user)
);

const loadUser = () => {
  fetch("http://localhost:3000/users/1")
    .then(response => response.json())
    .then(user => resolveUser(User.decode(user)))
    .catch(error => {
      console.log({ error });
    });
};
```

As we can see from the above code, 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 = () => {
  fetch("http://localhost:3000/users/1")
    .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 like error reporters or 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)