Introduction
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.
And action …
In this section we will have a look how our code base changed using that neat library.
Before
Let’s have a look how we previously would have implemented the redux part of a signup form.
1 | // actions.ts |
As you can see we created the type constants and action creators like in usual good old JavaScript (plus type declarations). The reducers file in TypeScript also doesn’t differ much from one written in JavaScript:
1 | // reducers.ts |
There is an interface SignupFormState
declared for the redux state of our signup form. It is used in the angle brackets of redux’s combineReducers
.
The argument 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.
After
1 | // actions.ts |
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.
1 | // reducers.ts |
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 type
, payload?
and 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.
1 | // actions.ts |
submitSignupForm
is itself a thunk action and therefore can be directly dispatched. Also it has properties request
, success
and 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:
1 | dispatch(submitSignupForm()); |
The dispatch
and 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.
Conclusion
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.