Advanced TypeScript with a Real Example (Part 2)

Published in WordPress.

Watch our video

There is a better version of your web

Share this post

It’s time to continue with (and hopefully finish) our TypeScript tutorial. If you have missed the previous posts we’ve written about TypeScript, here they are: our initial introduction to TypeScript, and the first part of this tutorial where I explain the JavaScript example we are working with and the steps we took to partially improve it.

Today we are going to finish our example by completing everything that’s still missing. Specifically, we will first see how to create types that are partial versions of other existing types. We will then see how to correctly type the actions of a Redux store using type unions, and we’ll discuss the advantages type unions offer. And, finally, I will show you how to create a polymorphic function whose return type depends on its arguments.

A Brief Review of What We’ve Done So Far…

In the first part of the tutorial we used (part of) a Redux store that we took from Nelio Content as our working example. It all started as plain JavaScript code which had to be improved by adding concrete types that make it more robust and intelligible. Thus, for example, we defined the following types:

type PostId = number;
type Day = string;

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

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

which helped us to understand, at a glance, the type of information our store works with. In this particular instance, for example, we can see that the state of our application stores two things: a list of posts (which we have indexed through their PostId) and a structure called days that, given a certain day, returns a list of post identifiers. We can also see the attributes (and their specific types) we’ll find in a Post object.

Once these types were defined, we edited all the functions of our example to use them. This simple task transformed JavaScript’s opaque function signatures:

// Selectors
function getPost( state, id ) { ... }
function getPostsInDay( state, day ) { ... }

// Actions
function receiveNewPost( post ) { ... }
function updatePost( postId, attributes ) { ... }

// Reducer
function reducer( state, action ) { ... }

to self-explanatory TypeScript function signatures:

// Selectors
function getPost( state: State, id: PostId ): Post | undefined { ... }
function getPostsInDay( state: State, day: Day ): PostId[] { ... }

// Actions
function receiveNewPost( post: Post ): any { ... }
function updatePost( postId: PostId, attributes: any ): any { ... }

// Reducer
function reducer( state: State, action: any ): State { ... }

The getPostsInDay function is a very good example of how much TypeScript will improve the quality of your code. If you look at the JavaScript counterpart, you really don’t know what that function is going to return. Sure, its name might hint the result type (is it a list of posts, maybe?), but you must look at the source code of the function (and probably the actions and reducers too) to be sure (it’s actually a list of post IDs). One can improve this situation by naming stuff better (getIdsOfPostsInDay, for example), but there’s nothing like concrete types to clean any doubt: PostId[].

So, now that you’re up to speed with the current state of affairs, it’s time to move on and fix everything we skipped last week. Specifically, we know we need to type the attributes attributes of the updatePost function and we need to define the types our actions will have (note that in reducer, the action attribute right now is of type any).

How to Type an Object whose Attributes are a Subset of Another Object’s

Let’s warm up by starting with something simple. The updatePost function generates an action that signals our intent of updating certain attributes of a given post ID. Here’s how it looks like:

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

and here’s how the action is used by the reducer to update the post in our store:

function reducer( state: State, action: any ): State {
  // ...
  switch ( action.type ) {
    // ...
    case 'UPDATE_POST':
      if ( ! state.posts[ action.postId ] ) {
        return state;
      }
      const post = {
         ...state.posts[ action.postId ],
         ...action.attributes,
      };
      return { ... };
  }
  // ...
}

As you can see, the reducer searches the post in the store and, if it’s there, it updates its attributes by overwriting them using those included in the action.

But what exactly are an action’s attributes? Well, they’re clearly something that looks similar to a Post, as they’re supposed to overwrite the attributes we may find in a post:

type UpdatePostAction = {
  type: 'UPDATE_POST';
  postId: number;
  attributes: Post;
};

but if we try to use this we will see that it does not work :

const post: Post = {
  id: 1,
  title: 'Title',
  author: 'Ruth',
  day: '2020-10-01',
  status: 'draft',
  isSticky: false,
};

const action: UpdatePostAction = {
  type: 'UPDATE_POST',
  postId: 1,
  attributes: {
    author: 'Toni',
  },
};

because we don’t want attributes to be a Post itself; we want it to be a subset of Post attributes (i.e. we want to specify only those attributes of a Post object that we’ll be overwriting).

To solve this problem, just use the Partial utility type :

type UpdatePostAction = {
  type: 'UPDATE_POST';
  postId: number;
  attributes: Partial<Post>;
};

And that’s it! Or is it?

Filtering Attributes Explicitly

The previous snippet is still faulty, as it’s possible to get some runtime errors that TypeScript’s compiler is not checking. Here’s why: the action that signals a post update thas two arguments, a post ID and the set of attributes we want to update. Once we have the action ready, the reducer is in charge of overwriting the existing post with the new values:

const post = {
  ...state.posts[ action.postId ],
  ...action.attributes,
};

And that’s precisely the faulty part in our code; it’s possible that the action’s postId attribute has a pots ID x and the id attribute in attributes has a different post ID y:

const action: UpdatePostAction = {
  type: 'UPDATE_POST',
  postId: 1,
  attributes: {
    id: 2,
    author: 'Toni',
  },
};

This is obviously a valid action, and so TypeScript doesn’t trigger any errors, but we know it shouldn’t be. The id attribute in attributes (if present) and the postId attribute should have the same value, or else we have an incoherent action. Our action type is imprecise because it lets us define a situation that should be impossible… so how can we fix this? Quite easily: just change this type so that this scenario that should be impossible becomes actually impossible.

The first solution I thought of is the following: remove the postId attribute from the action and add the ID in the attributes attribute:

type UpdatePostAction = {
  type: 'UPDATE_POST';
  attributes: Partial<Post>;
};

function updatePost( postId: PostId, attributes: Partial<Post> ): UpdatePostAction {
  return {
    type: 'UPDATE_POST',
    attributes: {
      ...attributes,
      id: postId,
    },
  };
}

Then, update your reducer so that it uses action.attributes.id instead of action.postId to find and overwrite the existing post.

Unfortunately, this solution is not ideal, because attributes is a “partial post,” remember? This means that, in theory, the id attribute may or may not be in the attributes object. Sure, we know it will be there, because we’re the ones generating the action… but our types are still imprecise. If in the future someone modifies the updatePost function and doesn’t make sure that attributes includes the postId, the resulting action would be valid according to TypeScript but our code would not work:

const workingAction: UpdatePostAction = {
  type: 'UPDATE_POST',
  attributes: {
    id: 1,
    author: 'Toni',
  },
};

const failingAction: UpdatePostAction = {
  type: 'UPDATE_POST',
  attributes: {
    author: 'Toni',
  },
};

So, if we want TypeScript to protect us, we must be as precise as possible when specifying types and make sure they make impossible states impossible. Considering all this, we only have two options available:

  1. If we have a postId attribute in action (as we did in the beginning), then the attributes object must not contain an id attribute.
  2. If, on the other hand, the action doesn’t have a postId attribute, then attributes must contain an id attribute.

The first solution can be easily specified using another utility type, Omit, which allows us to create a new type by removing attributes from an existing type:

type UpdatePostAction = {
  type: 'UPDATE_POST';
  postId: PostId,
  attributes: Partial< Omit<Post, 'id'> >;
};

which works as expected :

const workingAction: UpdatePostAction = {
  type: 'UPDATE_POST',
  postId: 1,
  attributes: {
    author: 'Toni',
  },
};

const failingAction: UpdatePostAction = {
  type: 'UPDATE_POST',
  postId: 1,
  attributes: {
    id: 1,
    author: 'Toni',
  },
};

For the second option, we have to explicitly add the id attribute on top of the Partial<Post> type we defined:

type UpdatePostAction = {
  type: 'UPDATE_POST';
  attributes: Partial<Post> & { id: PostId };
};

which, again, gives us the expected result :

const workingAction: UpdatePostAction = {
  type: 'UPDATE_POST',
  attributes: {
    id: 1,
    author: 'Toni',
  },
};

const failingAction: UpdatePostAction = {
  type: 'UPDATE_POST',
  attributes: {
    author: 'Toni',
  },
};

Union Types

In the previous section, we’ve already seen how to type one of the two actions our store has. Let’s do the same with the second action. Knowing that receiveNewPost looks like this:

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

its type can be defined as follows:

type ReceiveNewPostAction = {
  type: 'RECEIVE_NEW_POST';
  post: Post;
};

Easy, right?

Now let’s take a look at our reducer: it takes a state and an action (whose type we don’t know yet) and produces a new State:

function reducer( state: State, action: any ): State { ... }

Our store has two different types of actions: UpdatePostAction and ReceiveNewPostAction. So what’s the type of the action argument? One or the other, right? When a variable can accept more than one type A, B, C, and so on, its type is a union of types. That is, its type can either be A or B or C, and so on. A union types is a type whose values can be of any of the types specified in that union.

Here’s how our Action type can be defined as a union type:

type Action = UpdatePostAction | ReceiveNewPostAction;

The previous snippet is simply stating that an Action can be either an instance of the UpdatePostAction type or an instance of the ReceiveNewPostAction type.

If we now use Action in our reducer:

function reducer( state: State, action: Action ): State { ... }

we can see how this new version of our code based, which is well-typed, works smoothly.

How Union Types Eliminate Default Cases

“Wait a second,” you might say, “the previous link isn’t working smoothly, the compiler is triggering an error!” Indeed, according to TypeScript, our reducer contains unreachable code:

function reducer( state: State, action: Action ): State {
  // ...
  switch ( action.type ) {
    case 'RECEIVE_NEW_POST': // ...
    case 'UPDATE_POST': // ...
  }
  return state; //Error! Unreachable code
}

Wait, what? Let me explain what’s going on here…

The Action union type we created is actually a discriminated union type. A discriminated union type is a union type in which all of its types share a common attribute whose value can be used to discriminate one type from the other.

In our case, the two Action types have a type attribute whose values are RECEIVE_NEW_POST for ReceiveNewPostAction and UPDATE_POST for UpdatePostAction. Since we know that an Action is, necessarily, an instance of either one action or the other, the two branches of our switch cover all the possibilities: either action.type is RECEIVE_NEW_POST or it is UPDATE_POST. Therefore, the final return is redundant and will be unreachable.

Suppose, then, that we remove that return to fix this error. Did we gain anything, beyond removing unnecessary code? The answer is yes. If we now add a new type of action in our code :

type Action =
  | UpdatePostAction
  | ReceiveNewPostAction
  | NewFeatureAction;

type NewFeatureAction = {
  type: 'NEW_FEATURE';
  // ...
};

suddenly the switch statement in our reducer will no longer covers all possible scenarios:

function reducer( state: State, action: Action ): State {
  // ...
  switch ( action.type ) {
    case 'RECEIVE_NEW_POST': // ...
    case 'UPDATE_POST': // ...
    // case NEW_FEATURE is missing...
  }
  // return undefined is now implicit
}

This means the reducer might implicitly return an undefined value if we invoke it using an action of type NEW_FEATURE, and that’s something that doesn’t match the function’s signature. Because of this mismatch, TypeScript complains and lets us know we’re missing a new branch to deal with this new action type.

Polymorphic Functions with Variable Return Types

If you’ve made it this far, congratulations: you’ve learned everything you need to do to improve the source code of your JavaScript applications using TypeScript. And, as a reward, I am going to share with you a “problem” that I came across a few days ago and its solution. Why? Because TypeScript is a complex and fascinating world and I want to show you up to which extend this is true.

At the beginning of this whole adventure, we’ve seen one of the selectors we have is getPostsInDay and how its return type is a list of post IDs:

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

even though the name suggests it might return a list of posts. Why did I use such a misleading name, you’re wondering? Well, imagine the following scenario: suppose you want this function to be able to either return a list of post IDs or return a list of actual posts, depending on thevalue of one of its arguments. Something like this:

const ids: PostId[] = getPostsInDay( '2020-10-01', 'id' );
const posts: Post[] = getPostsInDay( '2020-10-01', 'all' );

Can we do that in TypeScript? Of course we do! Why else would I bring this up otherwise? All we have to do is define a polymorphic function whose result depends on the input parameters.

So, the idea is we want two different versions of the same function. One should return a list of PostIds if one of the attributes is the string id. The other should return a list of Posts if that same attribute is the string all.

Let’s create them both:

function getPostsInDay( state: State, day: Day, mode: 'id' ): PostId[] {
  // ...
}
function getPostsInDay( state: State, day: Day, mode: 'all' ): Post[] {
  // ...
}

Easy, right? WRONG! This doesn’t work. According to TypeScript, we have a “duplicate function implementation.”

Okay, let’s try something different, then. Let’s merge the previous two definitions into a single function:

function getPostsInDay( state: State, day: Day, mode: 'id' | 'all' = 'id' ): PostId[] | Post[] {
  if ( 'id' === mode ) {
    return state.days[ day ] ?? [];
  }
  return [];
}

Does this behave as we want? I’m afraid it doesn’t…

Here’s what this function signature is telling us: “getPostsInDay is a function that takes two arguments, a state and a mode whose values can either be id or all; its return type will either be a list of PostIds or a list of Posts.” In other words, the previous function definition doesn’t specify anywhere that there’s a relationship between the value given to the mode argument and the function’s return type. And so code like this:

const state: State = { posts: {}, days: {} };
const ids: PostId[] = getPostsInDay( state, '2020-10-01', 'id' );
const posts: Post[] = getPostsInDay( state, '2020-10-01', 'all' );

is valid and doesn’t behave like we want it to.

OK, last attempt. What if we mix our initial intuition, where we describe concrete function signatures, with a single, valid implementation?

function getPostsInDay( state: State, day: Day, mode: 'id' ): PostId[];
function getPostsInDay( state: State, day: Day, mode: 'all' ): Post[];
function getPostsInDay( state: State, day: Day, mode: 'id' | 'all' ): PostId[] | Post[] {
  const postIds = state.days[ day ] ?? [];
  if ( 'id' === mode ) {
    return postIds;
  }
  return postIds
    .map( ( pid ) => getPost( state, pid ) )
    .filter( ( p ): p is Post => !! p );
}

The previous snippet has a valid function implementation that works, but defines two extra function signatures that bind concrete values in mode with the function’s return type.

Using this approach, this code is valid:

const ids: PostId[] = getPostsInDay( state, '2020-10-01', 'id' );
const posts: Post[] = getPostsInDay( state, '2020-10-01', 'all' );

and this one doesn’t:

const ids: PostId[] = getPostsInDay( state, '2020-10-01', 'all' );
const posts: Post[] = getPostsInDay( state, '2020-10-01', 'id' );

Conclusions

In this series of posts we have seen what TypeScript is and how we can apply it in our projects. Types help us to better document the code by providing semantic context. Moreover, types also add an extra layer of security, since the TypeScript compiler takes care of validating that our code fits together correctly, just like Legos do.

At this point you already have all the necessary tools to take the quality of your work to the next level. Good luck in this new adventure!

Featured image by Mike Kenneally 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.