Matching your way to consistent states
Tags: typescript, functional, patterns, codequality
Published: 2019-10-03 10:32:19

---
title: Matching your way to consistent states
published: true
description: Finding a safer and more declarative pattern to model state in TypeScript by using discriminated unions
tags: typescript, functional,patterns,codequality
cover_image: https://thepracticaldev.s3.amazonaws.com/i/5r5f42qda7qluc9yg9ie.jpg
---

In the frontend team at [HousingAnywhere](https://housinganywhere.com/), we've been writing our React and Node codebases with TypeScript since the end of 2017. We have React powered applications and a few Node services for server-side rendering and dynamic content routing. As our codebase was growing, and more and more code was reused across different pages and modules, we decided we could benefit from a compiler that would guide us. Since then, we've been migrating one file or module at a time. Most of our new code is written in TypeScript, and most of the codebase has been migrated as well.

Since then, we've been looking for ways to leverage the type system to help us make sure our code is correct.

One of the things we realized is that we aren't writing JavaScript anymore. TypeScript is a different language. Yes, most of the semantics of the language are the same, as is the syntax (excluding the types). This is the goal, after all, to build a superset of JavaScript. But it also has a compiler that can guide and helps us write correct code (or at least attempt to do so), which means we can now model many of our problems using types.

This realization wasn't an “Aha” moment, but more of a series of changes we went through and patterns we adopted that enabled us to benefit more from the type system. I want to share one of those patterns with you in this blog.

## Time for Modeling States

When coding, it’s very often the case that we have to handle several variants as part of the control flow of our applications. Sometimes it's the status of a request, the state of a component and what needs to be rendered, or the items that need to be displayed from which a user may choose.

In this article, I’ll explain how you can model the status of a request and decide what outcome you want to show to the user as a result. The patterns discussed here can be applied to any other case where only one boolean isn't enough. I’ll walk you through different ways to approach this — from the more traditional imperative style, to a more declarative one that leverages the type system which makes it safer.

In our status, we have four potential scenarios, which we call `LoadingStatus`: an initial one, before any request is made. There’s also a loading one, while we are waiting for the request to be fulfilled. And lastly, there are success or error cases.

One approach we can take is to use a combination of boolean properties to distinguish between them. This is a very common approach to modeling such statuses and is often the default approach people use. Our first version of `LoadingState` looks like this:

```ts
interface LoadingState<T> {
  isLoading: boolean;
  isLoaded: boolean;
  error?: string;
  data?: T;
}
```

As I mentioned before, there are several valid combinations possible.

```ts
const notAsked = {
  isLoading: false,
  isLoaded: false,
  error: undefined,
  data: undefined,
};
```

<figcaption>Before any request is made.</figcaption>

```ts
const loading = {
  isLoading: true,
  isLoaded: false,
  error: undefined,
  data: undefined,
};
```

<figcaption>While the request is pending.</figcaption>

```ts
const success = {
  isLoading: false,
  isLoaded: true,
  error: undefined,
  data: someData,
};
```

<figcaption>A successful resolution 🎉</figcaption>

```ts
const failure = {
  isLoading: false,
  isLoaded: true,
  error: "Fetch error",
  data: undefined,
};
```

<figcaption>A failed resolution 😞</figcaption>

We can use that loading state in a React component to show the status and data to the user.

```jsx
const MyComponent = ({loadingState}) => {
  if (loadingState.isLoading) {
    return <Spinner />;
  }

  if (loadingState.error) {
    return <Alert type="danger">There was an error</Alert>;
  }

  if (loadingState.isLoaded) {
    return <Content data={loadingState.data} />;
  }

  return <EmptyContent />;
};
```

This works well for all the valid states. Now, what if our state looks like this? What should be displayed to the user?

```ts
const what = {
  isLoading: true,
  isLoaded: true,
  error: "Fetch error",
  data: someData,
};
```

Something clearly went wrong for us to get into this state. An option could be to accept that _"this is how it is: data can be inconsistent"_ and write tests to prevent our code from getting to this inconsistent state. Another approach would be to assume that an inconsistent state is another kind of error and write code that checks the state for inconsistencies, and show an error to the user when this happens.

But wait a minute. Since our code is now statically typed, is there something that could be done to solve this problem?

I’ll go through a series of improvements showing the potential and more generic ones for you to consider, and end with the version I would actually recommend using. _Disclaimer: these patterns are not very common and could require some effort to get your team on board. That said, I believe you’ll find that they’ll be worth the extra effort (in my opinion, it’s not that much extra effort, anyway)._

Here we go:

## Matching cases: One property to decide them all

For the first, case we’ll start with the most generic pattern. Since all the cases are mutually exclusive (e.g. there shouldn’t be data and an error at the same time), one step in the right direction would be to define all the cases as a union type.

```ts
type Status =
  | "not_asked"
  | "loading"
  | "success"
  | "failure";

interface LoadingState<T> {
  status: Status;
  data?: T;
  error?: string;
}
```

Now that we have one value as the source of truth of our status, we can update our component.

```jsx
const MyComponent = ({loadingState}) => {
  if (loadingState.status === "not_asked") {
    return <EmptyContent />;
  }

  if (loadingState.status === "loading") {
    return <Spinner />;
  }

  if (loadingState.status === "failure") {
    return (
      <Alert type="danger">
        {loadingState.error || "There was an error"}
      </Alert>
    );
  }

  if (laodingState.status === "success") {
    return loadingState.data ? (
      <Content data={loadingState.data} />
    ) : null;
  }
};
```

It’s much better already. We (_almost_) solved the inconsistency problem, as our status is defined by only one value now. Before we get into why I say _we almost solved the problem_, let’s tidy things up a bit. One of the goals I mentioned at the beginning was to make our code more declarative. However, `if` statements are by definition imperative. If we take a step back and consider what we’re doing, we’re trying to match each variant in a way so it can handle that specific case. This can be translated into a very short and simple, yet powerful utility function.

```ts
type Matcher<Keys extends string, R> = {
  [K in Keys]: (k: K) => R;
};

const match = <Keys extends string, R = void>(
  m: Matcher<Keys, R>,
) => (k: Keys) => m[k](k);
```

If we remove the types, we’re left with a function that takes an object and returns a function that takes a string, and uses that string to lookup a property of the object and call it with the string. Yeah, it’s even longer in English than it is in code.

```ts
const match = (m) => (k) => m[t](k);

match({foo: () => "bar"})("foo"); // => 'bar'
```

The implementation is simple and doesn’t say much, so let’s take a look at the types. We’re providing the keys of the object as a type parameter. It has to extend the string, meaning it can be an enum or a union type of strings. This guarantees that the object must have all the keys defined, providing an [exhaustiveness check](http://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking) (i.e. the compiler gives an error if one of the properties is missing).

Now, we can update our code once more.

```jsx
const MyComponent = ({loadingState}) => (
  match < Status,
  React.ReactNode >
    {
      not_asked: () => <EmptyContent />,
      loading: () => <Spinner />,
      success: () =>
        loadingState.data ? (
          <Content data={loadingState.data} />
        ) : null,
      failure: () => (
        <Alert type="danger">
          {loadingState.error || "There was an error"}
        </Alert>
      ),
    }(loadingState.status)
);
```

This makes things more declarative. Much better! The compiler will remind us if we forget to handle one of the cases, which is very convenient.

![compiler error on missing property](https://thepracticaldev.s3.amazonaws.com/i/24hfvsrwhl6mf6266nwd.png)

Besides making our code more declarative by expressing what each case should return instead of explicitly defining how each case should be determined, we’re also making the code easier to update in the future. As we add more cases to your union type, we don't have to search all over the place for code that needs updating. The compiler will simply inform us, and we can be sure that every case is covered.

At HousingAnywhere, we like this pattern a lot and not only use it for React components, but basically everywhere in our code (reducers, thunks, services, etc). Even though it is a simple and short module, we’ve made it available as a package: [@housinganywhere/match](https://github.com/housinganywhere/match).

## Fine Tuning: One last match

We’ve already made big improvements compared to the initial approach, and it scales very well thanks to the exhaustiveness check.

But as you might have seen in the previous examples, we still have to check the success and failure cases for undefined values. Why do we want to avoid this? Because we still have the chance to end up with inconsistent states that make no sense.

```ts
const leWhat = {
  status: "not_asked",
  data: someData,
  error: "Oops this makes no sense!",
};

const queEsEsto = {
  status: "success",
  data: undefined,
  error: undefined,
};
```

What we really want is for the compiler to guarantee us that we can only have consistent states. As mentioned in the beginning, when we consume the `LoadingState`, we’re basically matching each of the cases to handle them. We should, somehow, not only match the status but instead the whole shape of our state.

Enter [discriminated unions](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions) (also known as [tagged unions](https://en.wikipedia.org/wiki/Tagged_union) or [algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type)).

A discriminated union consist of a union of all of the cases, where each case should have a common, singleton type property, the discriminant or tag. By checking that _discriminant_ in our code with [type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types), the compiler is able to understand which case we are matching.

Let’s define our `LoadingState` once more, as a discriminated union this time.

```ts
type LoadingState<T> =
  | {status: "not_asked"}
  | {status: "loading"}
  | {status: "success"; data: T}
  | {status: "failure"; error: string};
```

Now we can implement a version of `match` that is tailored specifically for this version of `LoadingState`.

```ts
type LoadingStateMatcher<Data, R> = {
  not_asked: () => R;
  loading: () => R;
  success: (data: Data) => R;
  failure: (err: string) => R;
};

const match = <Data, R = void>(
  m: LoadingStateMatcher<Data, R>,
) => (ls: LoadingState<Data>) => {
  if (ls.status === "not_asked") {
    return m.not_asked();
  }

  if (ls.status === "loading") {
    return m.loading();
  }

  if (ls.status === "success") {
    return m.success(ls.data);
  }

  return m.failure(ls.error);
};
```

Again, we define a matcher object with methods for each of the cases of the status. But this time, each method has a different signature and will be called with the data or error when it should. Checking the _discriminant_ (status property) allows the compiler to understand the case we are in.

![refined cases](https://thepracticaldev.s3.amazonaws.com/i/8t6lyty6dzzerguihzll.png)

<figcaption>The compiler knows data is defined when status is 'success</figcaption>

We will update the usage one more time with the final version of our `match` utility.

```tsx
const MyComponent = ({loadingState}) =>
  match<SomeData, React.ReactNode>({
    not_asked: () => <EmptyContent />,
    loading: () => <Spinner />,
    success: (data) => <Content data={data} />,
    failure: (err) => <Alert type="danger">{err}</Alert>,
  })(loadingState.status);
```

Not only have we made our handling of the status more declarative and concise, but it’s also much safer, as we can’t get into those weird inconsistent states without the compiler ~~screaming~~ gently telling us we did something wrong.

Although these benefits may not seem like a big deal, they are quite significant. The whole point of a type system is not to have yet another source of errors to look at when we make a mistake in our code. Instead, its function is to help us write safer code, by modeling the code in such a way that we can identify that it’s correct at compile time.

## Further reading

_If you want to see this pattern to model other problems, take a look at these two examples_:

{% codesandbox p5rkn18mrj %}

{% codesandbox o9y2mrx5k9 %}

The ideas I present here aren’t new or exclusive to TypeScript. To learn more about them, you can take a look at how pattern matching (i.e. matching discriminated unions and data shapes) is implemented natively in other languages, and how type-driven development can be leveraged to write code that cannot be in inconsistent states.

- [Reasonml: Pattern matching](https://reasonml.github.io/docs/en/pattern-matching.html)
- [Making impossible state impossible](https://www.youtube.com/watch?v=IcgmSRJHu_8)
- [How Elm Slays a UI Antipattern](http://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipattern.html)
- [Remote Data implementation in TypeScript](https://github.com/devexperts/remote-data-ts)

Happy and safe coding! 🎉