Advanced TypeScript with a Real Example (Part 1)

Published in WordPress.

Watch our video

There is a better version of your web

Share this post

Last week we saw a little introduction to TypeScript, and specifically, we talked about how this language that extends JavaScript can help us create more robust code. Since that was just an introduction, I didn’t talk about some of the TypeScript features you might want (and probably need) to use in your projects.

Today I will teach you how to apply TypeScript professionally in a real project. To do this, we’ll start by looking at a portion of Nelio Content’s source code to understand where we start at and what limitations we currently have. Next, we will gradually improve the original JavaScript code by adding small incremental improvements until we have a fully-typed code.

Using Nelio Content’s source code as the basis

As you may already know, Nelio Content is a plugin that allows you to share the content of your website on social media. In addition to this, it also includes several functionalities that aim to help you constantly generate better content on your blog, such as a quality analysis of your posts, an editorial calendar to keep track of the upcoming content you need to write, and so on.

Nelio Content's editorial calendar
Nelio Content’s editorial calendar.

Last month we published version 2.0, a complete redesign both visually and internally of our plugin. We created this version using all the new technologies we have available in WordPress today (something we’ve talked about recently in our blog), including a React interface and a Redux store.

So, in today’s example we’ll be improving the latter. That is, we’ll see how we can type a Redux store.

Nelio Content Editorial Calendar Selectors

The editorial calendar is a user interface that shows the blog posts we have scheduled for each day of the week. This means that, at a minimum, our Redux store will need two query operations: one that tells us the posts that are scheduled on any given day, and another one that, given a post ID, returns all of its attributes.

Assuming you have read our posts on the subject, you already know a selector in Redux receives as its first parameter the state with all the information of our app followed by any additional parameters it might need. So our two example selectors in JavaScript would be something like this:

function getPost( state, id ) {
  return state.posts[ id ];
}

function getPostsInDay( state, day ) {
  return state.days[ day ] ?? [];
}

If you’re wondering how I know that a state has the posts and days attributes, it’s pretty simple: because I’m the one who defined them. But here’s why I decided to implement them like this.

We know that we want to be able to access our information from two different points of view: posts in a day or posts by ID. So it seems that it makes sense to organize our data in two parts:

  • On the one hand, we have a posts attribute in which we have listed all the posts that we have obtained from the server and saved in our Redux store. Logically, we could have saved them in an array and made a sequential search to find the post whose ID matches the expected one… but an object behaves like a dictionary, offering faster searches.
  • On the other hand, we also need to access posts that are scheduled on a certain day. Again, we could have used just a single array to store all the posts and filter it to find the posts that belong to a certain day, but having yet another dictionary offers a faster look-up solution.

Actions and Reducers in Nelio Content

Finally, if we want a dynamic calendar, we must implement functions that allow us to update the information our store stores. For simplicity, we are going to propose two simple methods: one that allows us to add new posts to the calendar and another that allows us to modify the attributes of the existing ones.

Updates to a Redux store require two parts. On the one hand, we have actions that signal the change we want to make and, on the other, there’s a reducer that, given the current state of our store and an action requesting an update, applies the necessary changes to the current state to generate a new state.

So, taking this into account, these are the actions we might have in our store:

function receiveNewPost( post ) {
  return {
    type: 'RECEIVE_NEW_POST',
    post,
  };
}

function updatePost( postId, attributes ) {
  return {
    type: 'UPDATE_POST',
    postId,
    attributes,
  }
}

and here’s the reducer:

function reducer( state, action ) {
  state = state ?? { posts: {}, days: {} };
  const postIds = Object.keys( state.posts );
  switch ( action.type ) {
    case 'RECEIVE_NEW_POST';
      if ( postIds.includes( action.postId ) ) {
        return state;
      }
      return {
        posts: {
          ...state.posts,
          [ action.post.id ]: action.post,
        },
        days: {
          ...state.days,
          [ action.post.day ]: [
            ...state.days[ action.post.day ],
            action.post.id,
          ],
        },
      };

    case 'UPDATE_POST';
      if ( ! postIds.includes( action.postId ) ) {
        return state;
      }
      const post = {
         ...state.posts[ action.postId ],
         ...action.attributes,
      };
      return {
        posts: {
          ...state.posts,
          [ post.id ]: post,
        },
        days: {
          ...Object.keys( state.days ).reduce(
            ( acc, day ) => ( {
              ...acc,
              [ day ]: state.days[ day ].filter(
                ( postId ) => postId !== post.id
              ),
            } ),
            {}
          ),
          [ post.day ]: [
            ...state.days[ post.day ],
            post.id,
          ],
        },
      };
  }
  return state;
}

Take your time to understand it all and let’s move forward!

From JavaScript to TypeScript

The first thing we should do is translate the previous code into TypeScript. Well, since TypeScript is a superset of JavaScript, it already is… but if you copy and paste the previous functions into the TypeScript Playground, you will see that the compiler complains quite a bit because there are too many variables whose implicit type is any. So let’s fix that first by explicitly adding some basic types.

All we have to do is explicitly add the any type to anything that is “complex” (such as the state of our application) and use number or string or whatever it is that we want to any other variable/argument. For example, the original JavaScript selector:

function getPost( state, id ) {
  return state.posts[ id ];
}

with explicit TypeScript types would look like this:

function getPost( state: any, id: number ): any | undefined {
  return state.posts[ id ];
}

As you can see, the simple action of typing our code (even when we use “generic types”) offers a lot of information with a quick glance; a clear improvement compared to basic JavaScript! In this case, for instance, we see that getPost expects a number (a post ID is an integer, remember?) and the result will either be something if the post exists (any) or nothing if it doesn’t (undefined).

Here you have the link with all the code type using simple types so that the compiler doesn’t complain.

Create and Use Custom Data Types in TypeScript

Now that the compiler is happy with our source code, it’s time to think a bit about how we can improve it. For this, I always propose to start by modeling the concepts that we have in our domain.

Creating a Custom Type for Posts

We know our store will contain posts primarily, so I’d argue the first step is to model what a post is and what information we have about it. We already saw how to create custom types last week, so let’s give it a shot today with the post concept:

type Post = {
  id: number;
  title: string;
  author: string;
  day: string;
  status: string;
  isSticky: boolean;
};

No surprises here, right? A Post is an object that has a few attributes, such a numeric id, a text title, and so on.

Another important piece of information any Redux store has is, you guessed it, its state. In the previous section we have already discussed the attributes it has, so let’s define the basic shape of our State type:

type State = {
  posts: any;
  days: any;
};

Improving the State Type

Now we know State has two attributes (posts and days), but we don’t know much about what they are, as they can be anything. We said we wanted both attributes to be dictionaries. That is, given a certain query (either a post ID for posts or a date for days), we want the related data (a post or a list of posts, respectively). We know we can implement a dictionary using an object, but how do we represent a dictionary in TypeScript?

If we take a look at the TypeScript documentation, we will see that it includes several utility types to deal with fairly common situations. Specifically, there is a type called Record that seems to be the one we want: it allows us to type a variable using key/value pairs in which the key has a certain Keys type and the values are of type Type. If we apply this type to our example, we’d end up with something like this:

type State = {
  posts: Record<number, Post>;
  days: Record<string, number[]>;
};

From the compiler’s perspective, the Record type works in such a way that, given any value of Keys (in our example, number for posts and string for days), its result will always be an object of type Type (in our case, a Post or a number[], respectively). The problem is that’s not how we want our dictionary to behave: when we look for a specific post using its ID, we want the compiler to know that we may or may not find a related post, which means the result can either be a Post or undefined.

Luckily, we can easily fix this by using yet another utility type, the Partial type:

type State = {
  posts: Partial< Record<number, Post> >;
  days: Partial< Record<string, number[]> >;
};

Improving Our Code with Type Aliases

Take a look at the posts attribute in our state… What do you see? A dictionary that indexes Post type posts with numbers, right? Now picture yourself reviewing this code at work. If you encounter such a type, you might assume that a number indexing posts is probably the ID of the indexed posts… but that’s just an assumption; you’d have to review the code to be sure about it. And what about days? “Random strings indexing lists of numbers.” That’s not very helpful, is it?

TypeScript types help us write more robust code thanks to the compiler checks, but they offer way more than that. If you use meaningful types, your code will be better documented and will be easier to maintain. So let’s alias existing types to create meaningful types, shall we?

For example, knowing that post IDs (number) and dates (string) are relevant to our domain, we can easily create the following type aliases:

type PostId = number;
type Day = string;

and then rewrite our original types using these aliases:

type Post = {
  id: PostId;
  title: string;
  author: string;
  day: Day;
  status: string;
  isSticky: boolean;
};

type State = {
  posts: Partial< Record<PostId, Post> >;
  days: Partial< Record<Day, PostId[]> >;
};

Another type alias we can use to improve the readability of our code is the Dictionary type, which “hides” the complexity of using Partial and Record behind a convenient structure:

type Dictionary<K extends string | number, T> = Partial< Record<K, T> >;

making our source code clearer:

type State = {
  posts: Dictionary<PostId, Post>;
  days: Dictionary<Day, PostId[]>;
};

And that’s it! There you have it! With just three simple type aliases we were able to document the code in a way that’s clearly better than using comments. Any developer that comes after us will be able to know, at a glance, that posts is a dictionary that indexes objects of types Post using their PostId and that days is a data structure that, given a Day, returns a list post identifiers. That’s pretty awesome, if you ask me.

But not only type definitions themselves are better… if we use these new types throughout all our code:

function getPost( state: State, id: PostId ): Post | undefined {
  return state.posts[ id ];
}

it also benefits from this new semantic layer! You can see the new version of our typed code here.

Oh, by the way, keep in mind that type aliases are, from the compiler’s point of view, indistinguishable from the “original” type. This means that, for example, a PostId and a number are completely interchangeable. So don’t expect the compiler to trigger an error if you assign a PostId to a number or viceversa (as you can see in this little example); they simply serve to add semantics to our source code.

Next Steps

As you can see, you can type JavaScript code using TypeScript types incrementally and, in doing so, its quality and readability improves. In today’s post we have seen in some detail an example of a real implementation of a React + Redux application and we have seen how it could be improved with relatively little effort. But we still have a long way to go.

In the next post we will type all the remaining variables/arguments that are currently using the any type and we’ll also learn some advanced TypeScript feats. I hope you liked this first part and, if you did, please share it with your friends and colleagues.

Featured image by Danielle MacInnes on Unsplash.

Leave a Reply

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

I have read and agree to the Nelio Software Privacy Policy

Your personal data will be located on SiteGround and will be treated by Nelio Software with the sole purpose of publishing this comment here. The legitimation is carried out through your express consent. Contact us to access, rectify, limit, or delete your data.