While the official Flow docs present a solution to typing Redux modules, the way their example is designed implies copy/pasting the type definitions into each of the Redux modules. This is not optimal.
Let's examine their example:
type Action =
| { type: "FOO", foo: number }
| { type: "BAR", bar: boolean };
type ThunkAction = (dispatch: Dispatch, getState: GetState) => any;
type PromiseAction = Promise<Action>;
type GetState = () => State;
type Dispatch = (action: Action | ThunkAction | PromiseAction) => any;
Looking at the first three lines in above snippet, we see that their example has a static set of action types specified. This exposes an issue in applications where the state is spread into multiple modules, leading to duplicate code or other code smell.
Using the method from above snippet in such application, we have to either:
- Specify the types top-level and provide them access to all action types of our application
- Copy/paste these type definitions for each Redux module—or, rather, the level at which you split your action types
Both of these solutions lead to code smells.
Making Types Generic Using Generics
So how do we create these types in a generic way? Using generics!
Remember, the main problem in the original code is that the type Action
is static. Essentially, all we need to do is to make that value variable.
Here's my solution:
export type ThunkAction<S, A> = (
dispatch: Dispatch<S, A>,
getState: GetState<S>
) => any;
export type PromiseAction<A> = Promise<A>;
export type GetState<S> = () => S;
export type Dispatch<S, A> = (
action: A | ThunkAction<S, A> | PromiseAction<A>
) => any;
Whoah, that's a lot of generics!
Yeah, I know. But it's really not that complex:
For the purpose of following Flow's own practices and for brevity, generics are named by one letter. A
stands for "Action" and S
for "State". These are the two types that we have to make variable, because they're different for each Redux module.
Using generics we can require "arguments" to be passed where the types are used. Referring my solution, ThunkAction
requires two "arguments" to be passed, State and Action, so defining a thunk action could look like this:
type FetchFooActions = ActionFoo | ActionBar;
function fetchFoo(): ThunkAction<State, FetchFooActions> {
return (dispatch, getState) => { /* inside my mind I have a digital mind */ };
}
State
is the type definition for the state of our Redux module, and FetchFooActions
is a clear specification of the actions that are expected to be dispatched from calling fetchFoo
.
If you need to use PromiseAction
, GetState
or Dispatch
, simply supply those with their generics "arguments":
// Supply your State and Action types to Dispatch, State only to GetState
const cb = (dispatch: Dispatch<State, Action>, getState: GetState<State>) => { ... };
// Supply Action type to PromiseAction
function asyncAction(): PromiseAction<Action> { ... }
We have now untangled the four types, ThunkAction
, PromiseAction
, GetState
and Dispatch
, so they can be shared across the application without code smell. But we even enabled ourselves to be more specific in our typing simultaneously. You can see this by attempting to dispatch an unexpected action in my full example:
I hope this is valuable inspiration. Feel free to comment or ask questions :)
Top comments (0)