TypeScript in a Redux Frontend
When we started our new product (Relay, a platform for service providers), we first decided to use TypeScript for the server-side code. Since this went out quite well for us, we also introduced TypeScript for the frontend very early. Now our user interface’s code is entirely written in TypeScript.
Somehow we were never completely comfortable with TypeScript in our frontend. It didn’t feel very type-safe although every single file was written in TypeScript. That’s because the core of the application’s data was missing types when it came to reducers. A classic redux reducer is a function that has two arguments, the current state and an action that was dispatched and can be handled now. But the action’s payload’s type was of type
any, a type we should avoid when we want to use TypeScript correctly.
So we looked for a library to boost our redux code and finally have type-safe reducers. That’s where we learned about typesafe-actions, tried it out and added some custom functions that suit our needs.
In this section we will have a look how our code base changed using that neat library.
Let’s have a look how we previously would have implemented the redux part of a signup form.
There is an interface
SignupFormState declared for the redux state of our signup form. It is used in the angle brackets of redux’s
state in each reducer is now correctly inferred from
SignupFormState. Unfortunately, each second argument
action is of type
any. Of course we could set the type of the possible action(s) for each reducer individually, but this would require us to declare and export the interface of the action that would result from each action creator. This approach didn’t look very appealing to us, because it causes a lot of boilerplate code in order to have a nice TypeScript DX. Luckily, typesafe-actions is out there and provides a different way of writing actions and reducers.
Now this is much more concise than before, right? This is because there is no need to additionally define constants for the types and export them for the reducers. Let’s see how the reducers deal with dispatched actions when we do not reuse the type constants.
We still use combineReducers they way we used it before, but the code in the function call clearly changed. Reducers are no longer done by switch-case functions, instead
createReducer does this job now. It takes one generic argument which will be used to determine the return type of the reducer and one parameter argument which will serve as the reducer’s initial state. Now, to take care of dispatched actions you can chain
handleAction calls on the reducer. The first argument is an action creator and the second one is a reducer-like callback function that infers its parameter types from the action creator.
We didn’t import the
createReducer function from typesafe-actions but from typesafe-thunk-actions. Both of them export such a function with very similar behaviors. The one from typesafe-actions needs a type definition that is a union of all possible actions (you can read about that in their documentation) for each reducer (or you have a root action type) and then add a handler for that action. But we wanted to reduce boilerplate code to have a nice DX.
That’s why we started a project called typesafe-thunk-actions that comes with its won
createReducer function. A specification for the action types is not needed, because
handleAction can infer the action’s payload. But there is a drawback in this implementation. It can only infer standard actions, i.e. actions with attributes
meta?. If that doesn’t match your needs, you should consider going with the
createReducer function from typesafe-actions.
But the module also contains a function called
createAsyncThunkAction that deals with async action creators in a standardized way. Imagine we want to send our signup form to the backend and deal with the response in redux.
submitSignupForm is itself a thunk action and therefore can be directly dispatched. Also it has properties
failure which can be passed to the
handleAction function of a reducer. The generic parameters take one argument which mirrors the async function’s return type. You don’t have to specify it when it can be inferred from the async function.
The async function takes up to 3 arguments. The first one is the one you will call the resulting action creator later. In this case we specified
void. That means we can dispatch the function without an argument:
getState arguments are the same you might know from redux-thunk. In the example we use
getState to pass the form data from our redux state to the server.
Not only the reducers are really typesafe now, we also managed to write less code at the same time. typesafe-actions and typesafe-thunk-actions come in really handy with their create*Action and createReducer functions. They enhance your DX by bringing type safety to redux apps. For detailed API descriptions please visit the GitHub repositories of typesafe-actions and typesafe-thunk-actions.