Automatically infer TypeScript types in switch statements

Rares Matei
InstructorRares Matei
Share this video with your friends

Social Share Links

Send Tweet
Published 6 years ago
Updated 5 years ago

Using string literal types together with switch types, we can build powerful type guarded logic flows. In this lesson, we will look at how we can take advantage of this, and build a reducer that automatically infers the action and payload type based on which switch statement case we are in.

Instructor: [00:00] Here, I have a very simple Redux set up for a Todo application. There is a reducer function which accepts an action and also the previous state. The state is defined as having a Todos property that's an array of string.

[00:11] If we look at the type of action, we can see there is a very simple interface with the single property type, that's a string. We're using the very specific interface, because in Redux, all of the actions must have at least the type. They also have a bunch of concrete actions defined here that implement that action.

[00:28] There is an add action which not only has a type property, but it also has a payload, which represents the text of the Todo we want to add, and there's also remove all action. The intention for that is for it to remove all the Todo's that we currently have stored. It doesn't have a payload. It just has a type.

[00:45] If we go back to my reducer, I have a switch statement here responding to the different kinds of types we can receive. In the case of the add action, we're just going to append its payload to the list of current Todo's making sure we don't mutate the state.

[00:58] In the case of remove all action, we're just going to return an empty list of Todos. Now you'll notice, there is an error here. That's because, I have set the action argument of this reducer only has a pipe property, but we're trying to access a payload on it, so its rightfully failing.

[01:13] We could add the generic payload property to this action's interface, but we don't know what type this should be, so we have to make it an any. The problem with that is, it's not really type safe, we're not really verifying the type of this payload in here.

[01:28] We are expecting a string, but if the type of the payload of the add action is anything other than the string, this is not going to fail in its current state. However, the change internal explicitly cast the payload in here. This will make a type safe. It will ensure that if the payload is anything other than a string, it's going to throw an error.

[01:48] Imagine, we have to handle a lot more than two actions in here. Having to do this every time, we'll not only add some load to our reducer, but we'd also not being dry. We already specify that this add action class, goes with the string as its type, and the place where we define the action.

[02:07] Now, we know that each action type has a unique string as its type property. If we have a switch statement, for TypeScript to be able to infer this class automatically for each string, two things need to happen.

[02:19] The first one is that the type property of these actions can't be a generic string anymore. If they are all strings, TypeScript won't be able to tell them apart, but how can we be more specific than a string.

[02:30] There is a string literal type that allows us to set the type of a property to a very specific string. To set it to that, we can just remove the first string type declaration. If we hover over this now, we can see that its type is now the specific add string and not just the generic string.

[02:46] Very important to note here is that, if I remove the read-only declaration from here, this will revert back to being a generic string type. We can see that by hovering over it. That's because by removing the read-only flag, TypeScript can be sure in the more that this property won't be modified later on, and that might get some different value from this.

[03:06] We can't assume that it will only state this very specific string type. That's why it marks it as a generic string. Let's have the backend and apply the changes to the other action as well. Now that, we made the type of this property unique for each of our actions, I'll need to create a finite list of all the types, TypeScript needs to look at when searching for a class based on the string.

[03:30] Now that, I've created this union of only Todo actions. I can go back to my reducer. First, I'll need to import that. Instead of telling it that it will accept any generic action, I'll change it to tell it that it's only going to set this very specific set of actions.

[03:50] Now, if I remove this place where I'm casting, and I look at the payload of the action directly, I can see that the error went away. If I hover over the action in here, I can see there is an add action specifically.

[04:01] If I try to use the action in this context, then I hover over X, I can see there is of type removal. TypeScript is now inferring the action class just based on the value of its type property. This feature that were taking advantage of is called discriminated unions.

[04:21] Now, if I go back to my actions, and I want to add a new action, let's say remove one, which has a payload of type number, and they could take of the ID that we want to remove.

[04:33] I'll need to add it to my union and here. If I go back to my reducer, and I try to use it in a wrong way, for example by trying to add its payload which is of type number to my Todos array which is of type string, TypeScript is going to throw an error.

[04:50] Let me quickly fix that. This is great. We're now getting full type safety in each of our case statements and we just prevent the potential disastrous problem.

[05:01] Finally, just as an extra layer of protection, if we want to ensure that we didn't forget to handle all of the actions we set this reducer with handle, then we can just add the default case, where we simply assign the action to a variable of type never.

[05:14] Now, if I comment out one of the actions, we're going to get the error here. That's because, they never type sets that this value will never occur which sense we're not handling all of the actions. It might get to this default case. That's why it's complaining. This line really has no purpose for the run-time.

[05:33] It's simply a compile-time check. We have to ensure we always handle all of the actions. To recap, we just have to look at how we can take advantage of discriminated unions in TypeScript. To use it, we'll first need to restrict the types of actions we're looking at to something very specific like a string literal.

[05:49] This property is called a discriminant. It doesn't have to be named type. It can have any name as long as is the property shared by all of the other actions we're looking at this well. Then, we need to build a finite set of total possible actions TypeScript has to look at.

[06:05] This gives the confidence to associate a particular value for the type property with a specific action class.

- -
- -
~ 5 years ago

Just wondering, why not to use enums instead of strings? The flow will infer the correct type as well

Rares Matei
Rares Mateiinstructor
~ 5 years ago

You are correct, I could have definitely used enums. Actually, the NgRx example app uses enums as the recommended approach, as you only have the string value declared in one place in that case: https://github.com/ngrx/platform/blob/master/projects/example-app/src/app/books/actions/collection-api.actions.ts

But in this case, I wanted to keep the lesson a bit simpler, as there's quite a lot going on already. So I opted to work with string literal types directly.

Yellow Tiger
Yellow Tiger
~ 5 years ago

Qustion: Is it possible to get the ActionType from the return type of our action creators?

Something like this:

export function messagesFetchRequested(messageId: string) {
  return {
    type: actionTypes.MESSAGES_FETCH_REQUESTED,
    payload: messageId
  };
}

type Action = ReturnType<typeof messagesFetchRequested>

but in this example Action is:

type Action = {
    type: string;
    payload: string;
}

Rares Matei
Rares Mateiinstructor
~ 4 years ago

Yep, that should work!

Markdown supported.
Become a member to join the discussionEnroll Today