My first reaction was “well, you should use GraphQL.” Or, even if you don’t have control over your APIs, having two sources of truth feel very cumbersome just to validate data. I believe a better approach will be to generate a JSON validator based on the type information at compile time such as babel-blade, or react-docgen-typescript-loader. But then again, I don’t have a clear plan to achieve that, either.
In this part two, I am going to describe our team’s current best practices to make Typescript work for you when working with Redux.
Creating Type-safe Actions and Reducers
Properly typing Redux Container
Creating Type-safe Actions and Reducers
Considering how reducers are just simple functions that accept two arguments, you would expect Typescript to work well with those two. States do. But actions, because dispatch accepts any types of arguments, cannot be typed safely without developers’ involvement. If you don’t type your actions, your reducer will end up in the not-so-ideal state:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
You can catch some of type errors with unit tests, but you will miss some properties and lose easy refactoring provided by Typescript. To acheive type-safety before Typescript 2.8, you could use string enum:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
IOtherAction is needed so that Typescript won’t complain about default case in switch statement (that is, exhaustiveness checking). This works OK if you ignore the fact that there are essentially two duplicate type definitions in your action interfaces, and action creators. Starting with Typescript 2.8, you can use ReturnType to remove action interfaces. The code below is our way to type actions and reducers.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Typing Redux container components correctly is important to use, and test the components correctly. Before our team learned how to type components, we ended up with tests like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Before you try to type Redux container components properly, you need to understand the type definition of connect. Carefully read the code below I quoted from Redux type definition (comments are mine). The definition uses a lot of type overloading but I will go through some cases to help you understand what exactly goes on.
Please note that the definitions below are from @types/react-redux@5.0.19.
This is when you only need dispatch inside your container.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
As there are no arguments to connect, all connect will do is to inject dispatch<any> into props.
When you pass in mapStateToProps to connect
If you want to map only state to props, say for render only components, you
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
It almost looks like a magic as Redux type definition does a lot of heavy lifting for us. Let’s examine what actually happens inside the code above.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This^ connect definition is the overloaded type definition used. In the definition, mapStateToProps is expanded to
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
So Typescript will infer TStateProps, and State to be {query: string}, and AppState from the argument mapStateToProps. InferableComponentEnhancerWithProps is expanded to
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
And Typescript will infer P to be Props, and check whether the container component’s props is larger than the union of TStateProps, DispatchProp<any>, and TOwnProps.
If I put the logic above into code, it looks like the following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
typeTOwnProps=Omit<Props,keyofTStateProps|keyofDispatchProp<any>>;// this results in { data: any }. But this isn't necessary and you can use {} without a problem.
When you pass in both mapStateToProps and mapDispatchToProps to connect
This isn’t hard to understand once you understood how Redux type definition handles mapStateToProps. mapDispatchToProps is treated like mapStateToProps. For your reference, I included the overloaded type below.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This is also rather straightforward. Instead of merging TStateProps, TDispatchProps, and TOwnProps naively for the component definition, Connect will now depend on mergeProps to merge these props. The only additional check, (or inference) is whether mergeProps is of type (stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps): TMergedProps;.
What this means
First of all, congratulations on getting through all these different types! Now you get how Connect works. But, it turns out you don’t need to type things directly when you use Redux’s Connect. However, other HOC’s definitions will vary, and you will need to learn how their type systems work.
Extracredit (Typescript tips not related to Redux)
Know your types in React
Knowing React types helps your code to work with React seamlessly. Here is the usual go-to list for us.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The following code is an excerpt from react-intl. This type definition is straight-forward to set up, but expects the users of the library to know which props are injected into.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This is an easy-to-miss option when you first start using Typescript. You should use typeRoots option to avoid adding unnecessary dependencies.
Afterword
As we develop, and maintain our React apps, we have encountered many bugs. Based on our experience, the harder-to-track, and more critical bugs often stemmed from typeless part of the code. That is why we are determined to type things both comprehensively, and correctly. This isn’t the farthest we can go with Typescript, but this is where we are at, and I hope this article has helped you understand Typescript and Redux more deeply.
In this two-part post, I am going to go over the different flavors of Redux state management at Vingle and our thought process behind each iterations we went through over the last year and half. I hope this post guide how you structure your Redux states.
Genesis: Redux + Immutable.Map
My team chose React to create a small-scale mobile marketing website as a learning experiment. Our main project, at the time, was based on Rails, and Angular 1, and we were separating web applications from Rails to simplify, and speed up our deployment process. That meant we had to create everything from scratch: a new build pipeline, a new webpack configuration, while learning about the vast React ecosystem.
We heard that Redux simplifies debugging application states greatly, and, with the nightmarish memories of debugging Angular 1’s watchers, chose to adopt Redux. We also learned a bit about shouldComponentUpdate and React’s component lifecycle, and wanted to have an immutable state. I was already familiar with high-order immutable objects from my previous work (this), so Immutable.js was an obvious choice.
In the end, we have Redux setup looking like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Once we have gotten more used to React, and Redux, and proven that we could develop new features much faster on the mobile page, we started migrating our main web application to React. But unlike the proof-of-concept mobile page, this app would have dozens of routes and reducers, and much more complex components, so we chose to use Typescript for this app.
Unfortunately, Immutable.Map with different types of values (number, boolean, other Maps, or Lists, for example) does not play well with Typescript. The following is a Typescript definition of Immutable.Map:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
As you can see, there isn’t a good way to specify different types of a Immutable.Map’s values. So we ended up doing this hacky workaround.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
So we looked for a better way to tie Typescript and Immutable.js together. Then we found that there was another Immutable class called Immutable.Record and a library called typed-immutable-record. With the library, we created a type-safe Immutable Record:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
It took some time for us to understand how to scaffold Record interfaces correctly but we managed to create type-safe redux states with both dot notations, and helper methods like setIn or withMuations. However, as you can see from the code above, we had to create a large number of interfaces, especially when our states were deeply nested. Once we got the pattern down, it wasn’t difficult to follow the pattern but it was a lot of work which disincentivized our team to create smaller, and isolated reducers. But we didn’t know any better, so we carried on.
During a random conversation with an engineer at another startup, I learned about readonly properties in Typescript, and realized those properties could replace Immutable.js completely.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
By using Readonly interfaces, the scaffolding is reduced to a quarter by removing RecordPart, Record, and recordify. However, there is a problem with this approach when you need to update deeply; the case above UPDATED_TITLE is such an example. During the conversion, we had some codes go out of hand like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
We could solve this problem by adopting a deep merge library, but we feared that those libraries may not be type-safe. After giving some thoughts, we determined that the real problem was with the deeply nested structures of our states and planned to flatten the states by normalizing. Of the two popular normalizing libraries, redux-orm, and normalizr, we chose the latter for its simplicity.
Our final, and current version of redux looks like the following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
When I look back, part of me regret that we didn’t do more research which could have saved a lot of time; this collection of redux-related libraries would have been helpful, and normalizing is already in official Redux documention. However, part of me also feel like we would have never appreciated the utility of these libraries and techniques because we didn’t know the downsides of not using those libraries and techniques. And that is why I wrote this post; I hope you understand what problems lie ahead and save yourself some time.