Hackernoon logoThe Redux Type Flow (revisited in 2018) by@zhirzh

The Redux Type Flow (revisited in 2018)

Shirsh Zibbu Hacker Noon profile picture

@zhirzhShirsh Zibbu

Associate Software Engineer

Exploring type safety in Redux using Flow

Type safety in redux code has 3 components:

Actions are just plain old JS objects. They are created by action creators.

Action creators are functions that create action objects.

Reducers are functions that take 2 arguments — state and action. Amongst the two, adding proper type to action is a real challenge. We’ll see in a moment why that is the case.

Our Workbench

To get my points across, I’ll use a simple reducer function throughout this article. It stores a string message from an input with the default value of “Hello World”. The user can clear the input, update or reset it.

const CLEAR = 'CLEAR';
const RESET = 'RESET';
const UPDATE = 'UPDATE';
function reducer(state: string = '', action) {
switch (action.type) {
case CLEAR:
return '';
    case RESET:
return action.text;
    case UPDATE:
return action.text;
    default:
return state;
}
}
function clear() {
return { type: CLEAR };
}
function reset() {
return { type: RESET, text: 'Hello World' };
}
function update(text: string) {
return { type: UPDATE, text };
}

Manual types

The “big idea” is that each action creator creates an action of a unique shape — a property type with value of type string.

We can manually add types to the action objects and use them in the reducer as a union type.

type ClearAction = { type: 'CLEAR' };
type ResetAction = { type: 'RESET', text: 'Hello World' };
type UpdateAction = { type: 'UPDATE', text: string };
type Action =
| ClearAction
| ResetAction
| UpdateAction;
function reducer(state: string = '', action: Action) {
...
}

This simple approach just works and you can see it here. An added benefit of annotating code this way is that we can also add types to the action creators as well (see here).

...
function clear(): ClearAction {
return { type: CLEAR };
}
function reset(): ResetAction {
return { type: RESET, text: 'Hello World' };
}
function update(text: string): UpdateAction {
return { type: UPDATE, text };
}

Inferred comment types

In a small, simple project, this works just fine. But when you have multiple reducers and each reducer has multiple actions where each action object is a different shape, writing everything manually becomes an challenge in itself. We need a better system; one that has some degree of automation.

Let’s take a look at ResetAction:

type ResetAction = { type: 'RESET', text: 'Hello World' };

It is of the same shape as the return value of the reset() action creator.

function reset() {
return { type: RESET, text: 'Hello World' };
}

Instead of writing each action object type manually, we can:

  1. invoke the action creators
  2. convert the returned object into a static type
  3. use those type values in the Action type.

Luckily, we don’t really have to literally invoke the action creators. We can leverage Flow’s comment types that will execute code blocks at “compile time” and not runtime (see here).

/*::
// "compile time" execution code
const clearAction = clear();
const resetAction = reset();
const updateAction = update('');
*/
type ClearAction = typeof clearAction;
type ResetAction = typeof resetAction;
type UpdateAction = typeof updateAction;
type Action = 
| ClearAction
| ResetAction
| UpdateAction;
function reducer(state: string = '', action: Action) {
...
}

Unfortunately, since we’re inferring types for action objects by invoking their action creators, we cannot annotate the creators with the inferred types. It kind of makes sense since that would create a circular chain of dependence between the inferred types and the type annotations.

Note: In line:5, we pass an empty string to update() because it requires a string argument and Flow requires that we pass all arguments for proper function calls.

Extracting return type

One of the most legit looking solutions came from this github thread. One of the comments there directed me to this article by Shane Osbourne and then back to the thread.

The article talks about a type that “extracts” a function’s return type and using it on the action creators. A later comment in the thread distilled the idea behind this down to its essence: actions are the return type of action creators; not the other way round. To extract the return type of a function, we use a generic function type ReturnType.

type ReturnTypeHelper = <T>((...Array<any>) => T) => T;
type ReturnType<Fn> = $Call<ReturnTypeHelper, Fn>;

This works the same as the previous one (see here) and also cannot add types to action creators. But look on the bright side — less syntax!

Make the $Call

Now that we have a better understanding of the process required for adding types, we can further reduce the syntax by directly calling functions by using Flow’s utility type $Call (you already saw it in action above).

$Call<F> is a type that represents the result of calling the given function type F. This is analogous to calling a function at runtime, but at the type level; this means that function type calls happens statically, i.e. not at runtime.

This is what we get (see here).

type ClearAction = $Call<typeof clear>;
type ResetAction = $Call<typeof reset>;
type UpdateAction = $Call<typeof update, string>; // arguments
type Action = 
| ClearAction
| ResetAction
| UpdateAction;
function reducer(state: string = '', action: Action) {
...
}

Note: As per my experience, $Call has a tendency to just “not work” at times. I am yet to experiment with it to figure out the cause behind this. If you know something, do tell me.

Final verdict

In the end, it all comes down to your choice. If you want to have explicit types for action creators, you need to manually add type annotations. If not, use inferred types to ease up the process and reduce syntax.

I hope you find this useful in the long run. If you type your redux code in a different, possibly cool way, let me know in the comments section.

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.