TypeScript Friendly State Management
TypeScript allows you to write safer and more robust code, while also improving the developer experience with things like better autocomplete, jump to definition, and type inference. However, setting up and using TypeScript with state management libraries has notoriously been difficult. Things are getting better, but there is still room for improvement. In this article I go over the current state of using TypeScript with Redux, MobX, and Overmind, and discuss what an even better solution would look like.
Existing Solutions
There are several existing frameworks with partial support for TypeScript. In a lot of these cases though, TypeScript is an afterthought and getting it setup is cumbersome and painful.
Redux
Redux has detailed documentation on how to setup TypeScript, but like many other areas of Redux, there is a lot of boilerplate involved. Especially if you want to have async actions using libraries like Thunk or Saga.
Creating actions is one area where the amount of code you need to write for TypeScript is almost double the amount for JavaScript. Let’s take a look at the example from the Redux documentation.
The types file is basically duplicated in the actions file. This means that for every new action you create, you will need to create a new constant, create a new action type interface, and create the action creator. All this and you haven’t implemented any actual logic. This is boilerplate. Type checking reducers is a bit better, except you need to manually type the action and return a value instead of it being inferred.
The above examples show the effort required to make TypeScript play nice with standard Redux. What if we want async actions? When using Redux thunk you will have thunk actions with the type:
ThunkAction<void, StateType, ThunkExtraArguments, ActionType>
Typing this throughout your codebase, even for smaller apps, makes things much more complicated than they need to be. In one project at Prodo we ended up with the following file:
Even as someone involved in the project from the start, I struggle to understand at a glance what the code is doing. Onboarding employees to the project was difficult because they needed to learn all of this TypeScript overhead.
When connecting React components to the store, the most common pattern I’ve seen is using Props and EnhancedProps .Props is the type for props that will be passed by the parent component and EnhancedProps is the type for props that come from the connect function.
MobX
MobX is currently the second most popular state framework for the web. Up until recently TypeScript support was very limited when using the inject function. However, the support has been much nicer since mobx-react version 6.0 when it started relying on React hooks.
Defining your store and actions is fully typed.
Observing part of the store in a component is accomplished by creating a useStores hook.
and using it in a component wrapped with observe.
There are a few gotchas with this method, but they are well documented on the mobx-react website.
TypeScript support in MobX is much nicer than Redux, but there are other aspects of the library that make it not suitable for all projects, such as when you want time travel debugging and uni-directional data flow.
Overmind
Overmind is another library for managing state that offers a very minimal and friendly API. It is less popular than Redux or MobX, but has strong support behind it. It was developed in TypeScript itself so offers very good support. The online editor CodeSandbox has even started to adopt Overmind, TypeScript being one of the main reason
There are two approaches you can use when setting up TypeScript for Overmind in your project. The first is the declare module approach.
The benefit of this is that all of the imports coming from Overmind are typed to your application. The downside is that you can only have a single Overmind instance in your app. Overriding types for a library might also make experienced TypeScript users a bit uncomfortable.
The second and more common approach is explicitly typing everything.
In both of these approaches, you must type actions explicitly. Unfortunately when you are manually typing something, TypeScript inference is longer used and you have to manually specify the return types.
Using your state in a component can be done by first creating a hook:
And using it in your components
What we want
The TypeScript authors have done an amazing job making it fit into the existing JavaScript ecosystem. Community efforts like DefinitelyTyped work really well and allow you to type JavaScript libraries that were created before TypeScript even existed. However, libraries that were designed with TypeScript in mind from the start, offer a more seamless developer experience.
With that in mind, the following are some features we would like to see in a state management framework when using TypeScript.
- Type Inference
- Framework extensions are fully typed
- The initial state is fully typed
- Jump to definition works seamlessly
Prodo
Here at Prodo we have taken the ideas above and created our own state management framework. We believe it is a step in the right direction and will allow you to develop applications with the speed of JavaScript and with the security and developer benefits of TypeScript. In comparison to the libraries mentioned above, Prodo has an API most similar to Overmind.
Defining your state is as simple as creating an interface.
Your initial state is fully typed when you create the store.
This provider is a React context provider and can be used to wrap your root level component.
Actions can be defined anywhere and are fully typed. The following examples are possible by using the Babel plugin.
Components are similarly typed
The above code is from the current version of our framework. We are also experimenting with different syntax and ways of doing state management. A post describing this can be found here.
We have open-sourced Prodo on Github on https://github.com/prodo-dev/prodo. Please consider giving this repo a star if you like the direction we’re taking. You can also join our Slack community if you want to continue the discussion.