Exploring type safety in Redux using Flow Type safety in code has 3 components: redux Actions Action creators Reducer are just . They are created by action creators. Actions plain old JS objects are functions that create action objects. Action creators are functions that take 2 arguments — and . Amongst the two, adding proper type to is a real challenge. We’ll see in a moment why that is the case. Reducers state action action 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 the input, or it. clear update reset const = 'CLEAR';const = 'RESET';const = 'UPDATE'; CLEAR RESET UPDATE function (state: string = '', action) {switch (action.type) {case :return ''; reducer CLEAR case **RESET**: return action.text; case **UPDATE**: return action.text; **default**: return state; }} function () {return { type: CLEAR };} clear function () {return { type: RESET, text: 'Hello World' };} reset function (text: string) {return { type: UPDATE, text };} update Manual types The “big idea” is that each action creator creates an of a unique shape — a property with value of type . action type string We can manually add types to the action objects and use them in the reducer as a type. union type = { type: 'CLEAR' };type = { type: 'RESET', text: 'Hello World' };type = { type: 'UPDATE', text: string }; ClearAction ResetAction UpdateAction type =| ClearAction| ResetAction| UpdateAction; Action function reducer(state: string = '', action: ) {...} Action This simple approach and you can see it . An added benefit of annotating code this way is that we can also add types to the action creators as well (see ). just works here here ... function (): {return { type: CLEAR };} clear ClearAction function (): {return { type: RESET, text: 'Hello World' };} reset ResetAction function (text: string): {return { type: UPDATE, text };} update UpdateAction 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 action creator. reset() function reset() {return ;} { type: RESET, text: 'Hello World' } Instead of writing each action object type manually, we can: invoke the action creators convert the returned object into a static type use those type values in the type. Action Luckily, we don’t really have to invoke the action creators. We can leverage that will execute code blocks at “compile time” and not runtime (see ). literally Flow’s comment types here /*::// execution codeconst = clear();const = reset();const = update('');*/ "compile time" clearAction resetAction updateAction type = typeof clearAction;type = typeof resetAction;type = typeof updateAction; ClearAction ResetAction UpdateAction type =| ClearAction| ResetAction| UpdateAction; Action function reducer(state: string = '', action: ) {...} Action Unfortunately, since we’re inferring types for action objects by invoking their action creators, we 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. cannot line:5_, we pass an empty string to_ Note: In _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 looking solutions came from . One of the comments there directed me to by and then back to the thread. legit this github thread this article Shane Osbourne The article talks about a type that “extracts” a function’s return type and using it on the action creators. A in the thread distilled the idea behind this down to its essence: . To extract the return type of a function, we use a . later comment actions are the return type of action creators; not the other way round generic function type ReturnType type = <T>((...Array<any>) => T) => T;type <Fn> = $Call<ReturnTypeHelper, Fn>; ReturnTypeHelper **ReturnType** This works the same as the previous one (see ) and also cannot add types to action creators. But look on the bright side — less syntax! here 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 (you already saw it in action above). utility type [$Call](https://flow.org/en/docs/types/utilities/#toc-call) is a type that represents the result of calling the given . This is analogous to calling a function at runtime, but at the ; this means that function type calls happens statically, i.e. not at runtime. $Call<F> function type F type level This is what we get (see ). here type = <typeof clear>;type = <typeof reset>;type = <typeof update, string>; // arguments ClearAction $Call ResetAction $Call UpdateAction $Call type =| ClearAction| ResetAction| UpdateAction; Action 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 way, let me know in the comments section. cool
Share Your Thoughts