Prelude
When TypeScript complained about then
, I dove into Conditional Types and probably we don’t need actions interfaces.
In my actions.ts
file, I would usually write the function implementations and at the bottom I would define an interface which is a copy-paste exercise of the function names and their respective types using typeof
. It looks like the following:
Once I have the actions interface defined and exported, in the Component/Container, I might have a way to assign that interface probably like this:
Then further down below MyComponent
class, we write our usual mapDispatchToProps
to bind our imported actions
to dispatch
with bindActionCreators
. We can now call them as we normally would:
This approach looks and will work fine. TypeScript will tell you that actions
has funA
and funThunkB
available actions and your choice of IDE will likely have a popup listing them down. The funA
call is type-checked properly and will neatly tell you it’s expected to return an object of type { type: string, payload: { id: number }}
.
But soon enough we’ll find a couple of things off:
(1) First, TypeScript complains when using then
on this.props.actions.funThunkB
A red-squiggle on then
appears on this.props.actions.funThunkB(id, anObject).then(...)
(2) Second, it feels like the whole copy-paste exercise in interface IMyActions
is redundant.
TypeScript complains about then
Looking at the then
error, we find that it’s actually reasonable. As you recall we have this in our actions.ts
file:
And that is equivalent to the following:
So then
definitely doesn’t exist in its return type (dispatch: ThunkDispatch<IStoreState, void, Action<any>>, getState: any) => Promise<void>
.
Now how do we use then
without TypeScript complaining? By telling it that the return type is a Promise
. But in order to do that, we need a way to define funThunkB
as (id: number, data: object) => Promise<void>
, only then TypeScript will keep quiet.
The good news is redux-thunk
is on the way to do that for us. It has a yet-to-be-released tweak to its index.d.ts
to include a conditional type so that it also infers ThunkAction
return types.
As per #223, redux’s
bindActionCreators
only works nicely for standard action creators. This implementation infers the return type of ThunkActions so the application could respond accordingly…
Extracting from that PR, the addition is the following:
https://github.com/reduxjs/redux-thunk/pull/224/files#diff-b52768974e6bc0faccb7d4b75b162c99R45
This means bindActionCreators
returns an object of actions which are conditionally typed (Conditional Types).
Let’s break that down by splitting it to two parts: (A) actionCreators
parameter and (B) the main part:
(A) The actionCreators
is where the imported actions
is supplied and it is of type M
which extends ActionCreatorsMapObject
. ActionCreatorsMapObject
is just a key-value pair mapping of function names to their types just as IMyActions
is.
When the import of actions
happens, we get an object of actions
like this:
And when the actions
object is passed into bindActionsCreators
in the actionCreators
parameter, inferred type M
refers to the following.
(B) Now that we have some ideas of what M
type is, let’s look at the conditional type again:
In [N in keyof M]
, N
refers to the function names that are keys of the actions object of type M
. So M[N]
is referring to the type of the value — for example, the type of the value for key funA
is:
M[N]
of funA
The ReturnType<M[N]>
then points to its the return type:
ReturnType<M[N]>
of funA
So with this pattern in mind, let’s look at the condition ReturnType<M[N]> extends ThunkAction<any, any, any, any>
. This condition checks if the return type extends ThunkAction
type. Quite simply, if this is false it means it’s a standard action creator and its type shall remain M[N]
– funA
is a standard action creator and its type remains { type: string; payload: { id: number } };
.
What then happens if the return type extends ThunkAction
and is therefore not a standard action creator? In this case, we infer the return type of the return type to be the return type of the inferred type of the value. To illustrate that, let’s go back to the type definition of funThunkB
:
M[N]
is the type of the value given the property N
. In the case of funThunkB
, M[N]
is:
Following that, ReturnType<M[N]>
is:
Which makes ReturnType<ReturnType<M[N]>
:
Now circling back to redux-thunk
’s conditional type: If ReturnType<M[N]> extends ThunkAction<any, any, any, any>
condition is true, the type will then become (...args: Parameters<M[N]>) => ReturnType<ReturnType<M[N]>>
. So for funThunkB
, (...args: Parameters<M[N]>)
is (id: number, data: object)
and as we’ve seen above ReturnType<ReturnType<M[N]>>
is Promise<void>
.
bindActionCreators
therefore returns a type that looks roughly like the following:
TypeScript infers that a funThunkB
now returns a Promise
:tada:
What’s the implication? It simply means bindActionCreators
is able to derive the accurate types of the actions for both standard and thunk action creators we access the actions within our components.
So back to MyComponent
, we have:
Now we need to be able to access the action types from the bindActionCreators
in the mapDispatchToProps
.
When we get the type of mapDispatchToProps
using typeof
we infer the following:
typeof mapDispatchToProps
This makes ReturnType<typeof mapDispatchToProps>
the following:
ReturnType<typeof mapDispatchToProps>
Then in our class definition we add that as part of our component props .
This then would allow us to access all the types of the action creators including the thunks. And of course, TypeScript now learns what we’re trying to do and the then
red-squiggle disappears because we have typed the thunk action creators like funThunkB
to have a return type Promise
. The current version at the time of this writing is redux-thunk@2.3.0
and it has yet to include the new bindActionCreators
type that we need. Until the update is released, one can copy the new index.d.ts
to override the existing types.
Using actions interfaces feels redundant
This brings me to my second point that action interfaces like IMyActions
to type our redux actions may be redundant. In my opinion, if we’re not using IMyActions
type anywhere else other than in the context of using them within our components, it may be possible to do away with them and let bindActionCreators
derive the types as they are imported. This way, you still get the type checks while not expanding the codebase with extra types and interfaces to maintain.