Paracaidísta con el texto «Impossible» en el paracaídas, de Martin Wyall

In the first part of this TypeScript tutorial we saw what union types are and how to discern one from the other using type predicates. If you haven’t read that post, I recommend that you do so now, as it lays the foundations you’ll need to understand what we’ll be discussing today.

In this post, we’ll talk about “making impossible states impossible” using union types, and how a better data model leads to more robust and reliable code. In addition, we will also see, looking at Nelio Popups‘ source code, how TypeScript can assist us in writing better React components.

How to Make Impossible States Impossible

Let’s start with a simple example. Imagine you want to create a PostList component that displays a list of posts. When the component is first displayed, it makes a request to the server to load the list of posts (let’s assume you have an effect hook called usePosts for that). While it’s loading the data, it shows an animation; when posts are ready, it shows them. We might implement such a component as follows:

export const PostList = ( { postIds } ) => {
  const { isLoading, posts } = usePosts( postIds );
  if ( isLoading ) return <Spinner />;

  if ( ! posts.length ) {
    return <p>No posts</p>
  }//end if

  return (
    <div>
      { posts.map( ( post ) => ... ) }
    <div>
  );
}

According to the component description I just share and the previous snippet, we can deduce that usePosts may have the following return type:

type PostResult = {
  readonly isLoading: boolean;
  readonly posts?: ReadonlyArray< Post >;
}

that is, an object with a boolean isLoading attribute and an (optional) list of posts. You might argue if posts should be optional or not; I assume it is because, well, while data is being loaded, we don’t have any posts, right?

Anyhow, using the previous type definition, one can create the following instances:

// Result 1
{ isLoading: true }

// Result 2
{ isLoading: true, posts: [] }

// Result 3
{ isLoading: true, posts: [ {…}, {…}, … ] }

// Result 4
{ isLoading: false }

// Result 5
{ isLoading: false, posts: [] }

// Result 6
{ isLoading: false, posts: [ {…}, {…}, … ] }

which, as you can see in the TypeScript Playground, are all valid instances of the PostResult type. However, there are actually a few results that don’t make any sense. Specifically, results 2 and 3, on the one hand, and result 4, on the other, are weird.

If we look at results 2 and 3, we see that posts are still being loaded (that’s what the isLoading attribute indicates), and yet, at the same time, we do have some results available (there’s a list of posts). On the other hand, in result 4, we should have the list of posts available… but the attribute is not even set! That’s clearly wrong, isn’t it?

This is a clear example of a poor data model. With PostResult, we are able to represent impossible states. It should be impossible to “be loading results” and, at the same time, “have a list of posts.” It should also be impossible to “know the results are loaded” and “miss the posts attribute.” And yet both situations are possible in our data model.

Our goal as (good) programmers is to craft good software, and this means we have to “make impossible states impossible.” In our running example, we can achieve this by using the following union type (which, by the way, is discriminated by the isLoading attribute):

type PostResult = LoadingPostResult | LoadedPostResult;

type LoadingPostResult = {
  readonly isLoading: true;
};

type LoadedPostResult = {
  readonly isLoading: false;
  readonly posts: ReadonlyArray< Post >;
}

If we implement this new PostResult definition in the TypeScript Playground, we’ll see how results 2, 3, and 4 do indeed become invalid. Therefore, we now have a definition that perfectly captures reality and, as a result, TypeScript will help us make sure our constraints are properly guaranteed.

If you want to know more about the subject, I recommend this talk by Richard Feldman. He uses Elm in his talk, but the examples and insights he shares are very interesting nonetheless:

Defining (part of) the Nelio Popups Data Model

At the beginning of the post I promised you we’d be using a real example, so it’s about time we do so. If you take a look at the Nelio Popups source code on WordPress.org, you will see that there is a folder in src/common called types with several files in it where we define the main types of our popups. If we take one of those files randomly (for example, src/common/types/popups/style.ts) and look at its content, we’ll see the types it contains are quite similar to the one we’ve just discussed in the previous section:

// …
export type OverlaySettings =
  | {
      readonly isEnabled: false;
    }
  | {
      readonly isEnabled: true;
      readonly color: Color;
    };

// …
export type BorderSettings =
  | {
      readonly isEnabled: false;
    }
  | {
      readonly isEnabled: true;
      readonly radius: CssSizeUnit;
      readonly color: Color;
      readonly width: CssSizeUnit;
    };

That is, each type has a flag that tells us if a certain property is active or not and, if it is, then we have the additional attributes for setting it up. So, how does this help us write better code?

React Components

As you see, the types in Nelio Popups are pretty straightforward and simply apply the following principle: “make the impossible impossible”. Let’s see how such a principle can guide our development.

One of the example types I pulled from Nelio Popups is OverlaySettings. As its name and properties suggest, Nelio Popups may or may not include an overlay and, when they do, the user can define its color. A component that manages a setting of this would be as follows:

Popup overlay settings
Overlay settings of a popup.

with the following source code:

import * as React from '@wordpress/element';
import { ToggleControl } from '@wordpress/components';
import { _x } from '@wordpress/i18n';

import { ColorControl } from '@nelio/popups/components';
import { usePopupMeta } from '@nelio/popups/hooks';

export const OverlayControl = (): JSX.Element => {
  const [ overlay, setOverlay ] = usePopupMeta( 'overlay' );
  const { isEnabled } = overlay;
  const onChange = ( isChecked: boolean ) =>
    isChecked
      ? setOverlay( { color: '#000000cc', isEnabled: true } )
      : setOverlay( { isEnabled: false } );

  return (
    <>
      <ToggleControl
        label={ _x(
          'Add overlay behind popup',
          'command',
          'nelio-popups'
        ) }
        checked={ isEnabled }
        onChange={ onChange }
      />
      { isEnabled && (
        <ColorControl
          color={ overlay.color }
          onChange={ ( newColor ) =>
            setOverlay( {
              ...overlay,
              color: newColor,
            } )
          }
        />
      ) }
    </>
  );
};

There are several things in the previous snippet that are worth mentioning. First of all, notice how, given an overlay instance, we can access its isEnabled prop with no safe guards or checks. This is because, as you can see in the definition of its type (OverlaySettings), the attribute is the union type’s discriminator and it’s therefore defined in all the “subtypes” of the union type. Thus, we can destructure it safely.

Second, the JSX we return in the function has two parts. On the one hand, the component always returns a switch to turn the overlay setting on or off. That is, we have a subcomponent responsible of managing the isEnabled attribute. On the other hand, we have a second subcomponent to manage the overlay’s color. However, this second component is visible if, and only if, the overlay isEnabled, which makes perfect sense, because we only have a color prop when the overlay is indeed enabled.

At this point you may be wondering why we’re creating a union type if a simpler type like this would do the trick:

type OverlaySettings = {
  readonly isEnabled: true;
  readonly color: Color;
};

Good thinking! Well, as I said, being precise when describing our data model will help us in the long run. So let’s take a closer look at how, with such a succinct example, the former type is better than the latter.

Using the simpler type we just proposed, we could write the onChange method we find in our component in multiple ways:

const onChange1 = ( isChecked: boolean ): void =>
  isChecked
    ? setOverlay( { color: '#000c', isEnabled: true } )
    : setOverlay( { color: '#000c', isEnabled: false } );

const onChange2 = ( isChecked: boolean ): void =>
  setOverlay( { color: '#000c', isEnabled: isChecked } );

const onChange3 = ( isChecked: boolean ): void =>
  setOverlay( { ...overlay, isEnabled: isChecked } );

but, unfortunately, not all of them are correct. For example, in third variant we have no idea how overlay is currently defined: if we enable the overlay using onChange3, we’d be using the current values of overlay and, therefore, its current color. But what is its current color? Are we sure it has a correct value? Who did initialize it? When? What value did they use? What if we have the following?

const overlay = {
  isEnabled: false,
  color: '',
};

However, the first (union) type we proposed forces us to explicitly set a color when we enable the setting and onChange3 would not work. Instead, we must write a function that explicitly sets a value for the color:

const onChange = ( isChecked: boolean ): void =>
  isChecked
    ? setOverlay( { color: '#000c', isEnabled: true } )
    : setOverlay( { isEnabled: false } );

Pretty cool, huh? I know it’s a simple example, but can you imagine how many bugs we might be able to prevent by following this approach everywhere? And, hey, don’t take my word for it — look at how happy our users are with the result:

I am really enjoying using Nelio Popups (v1.0.6). The developers have done a good job in designing this blocks plugin. I like the attention to user interface detail I can see in the plugin. I wish more developers would create single-use block plugins like Nelio Popups that do one thing, do it well, and naturally fit into the WordPress Blocks ecosystem.

TheFrameGuy on WordPress.org

Conclusion

A good data model is one that reliably captures the reality we’re trying to represent in our code. That is, we need a model expressive enough to represent all reality, but no more. By making impossible states impossible, we make the compiler our best friend, as it’ll make sure we don’t make silly mistakes.

I hope you liked today’s post and learned something useful. If you have any insights or doubts you’d like to share, tell me in the comment section bellow and I’ll be happy to assist.

Featured Image by Martin Wyall on Unsplash.

Leave a Reply

Your email address will not be published. Required fields are marked *