DEV Community

loading...
Cover image for Typescript, 100% type-safe react-redux under 20 lines

Typescript, 100% type-safe react-redux under 20 lines

svehla profile image Jakub Švehla ・10 min read

TLDR:

We’re gonna implement a static-type layer on top of the Redux App. Our goal is to write a minimalistic but 100% type-safe code. To do it well, we‘re gonna write code that will be more about type inferring and creating the data connection than about writing types.

Final source-code usage previews:

Inferred redux state from reducers

Alt Text

Inferred union of all possible redux actions

Alt Text

Inferred returned value of selectors

Alt Text

Alt Text

Inferred nested action payload by action type inside of reducer switch-case

Alt Text

You can find the full redux-typescript app in my GitHub repository:
https://github.com/Svehla/redux-ts-preview

Prerequisites

If you're not 100% sure about your Typescript skills you can check these beginner sources:

Basic static types inferring:

Let's start

You could ask yourself. “**We can just read the official documentation and that’s it, right?” **Unfortunately, the official Redux Typescript guide is not suitable for our inferring mindset.

In my humble opinion, the official React-Redux guide contains a lot of programming bottlenecks like repeatable code and a lot of abstraction and complexity. I don’t recommend to be inspired by that, you should just prefer to continue with reading this Typescript article.

Redux is a simple tool that is used to handle state management in modern web apps. Unfortunately Redux has some patterns which add a lot of unnecessary abstraction for a simple state management library. You have to create tons of functions that communicate over one black-box (Redux) which takes them all and makes some state changes and updates. Another problem with Redux is that there are no statically-analyzed source code connections, so you as a programmer don’t see dependencies and relationships between your Javascripts objects and functions. It’s like throwing functions into the air and check if it all works correctly. Of course Redux has a lot of useful features so it’s not bad at all. For example, Redux dev-tools are nice and you can simply use them as there are. Redux is also useful for large teams. Especially in a place where a lot of people contribute to the same repository at the same time.

Let’s have a look at Redux architecture. There are some middlewares, reducers, selectors, actions, thunks and at top of it, there is a Redux the black-box library which merges all pieces together and creates a global store.

In the diagram below we have the basic Redux data flow.

Alt Text

Data flow is simple and straightforward, which is awesome right?

So let’s have a look at another diagram, which shows the basics of Javascript source code relations with the usage of Redux.

Alt Text

Redux forces you to write a lot of small functions that are all merged together in the heart of the Redux library, so it’s hard to do static-analyses and find relations between these pieces of abstractions

Let’s add static-types

So our target is to create some Typescript glue that connects all these abstract parts (sectors, actions creators, reducers, etc…) together and makes Redux statically-analyzable, readable and type-safe.

Code snippets from this article are from this react-typescript repo:
https://github.com/Svehla/redux-ts-preview

Action creators

Action creators are functions that return a new object that is dispatched into Redux.

const MULTIPLY = 'MULTIPLY' as const 
const DIVIDE = 'DIVIDE' as const
const multiply = (multiplyBy: number) => ({
  type: MULTIPLY,
  multiplyBy,
})
const divide = (divideBy: number) => ({
  type: DIVIDE,
  divideBy,
})
Enter fullscreen mode Exit fullscreen mode

We’re gonna add a few Typescript types which help us to create data-types for action creators.

  1. We have to use as const for setting up action names like the enum value for future pattern-matching.
  2. We have to add types for function arguments
  3. We create ActionsType enum which enables us to logically connect actions to a reducer.
// global uniq names
// use `as const` for fixing value of type
const MULTIPLY = 'MULTIPLY' as const
const DIVIDE = 'DIVIDE' as const
const multiply = (multiplyBy: number) => ({
  type: MULTIPLY,
  multiplyBy,
})
const divide = (divideBy: number) => ({
  type: DIVIDE,
  divideBy,
})
// create options type for all action creators
// for one reducer. all data types are inferred
// from javascript so you don't have to
// synchronize types with implementations
type ActionType =
  | ReturnType<typeof multiply>
  | ReturnType<typeof divide>
Enter fullscreen mode Exit fullscreen mode

Reducer State

Each reducer has a state. Let’s define the basic one.

const defaultState = {
  value: 10
}
Enter fullscreen mode Exit fullscreen mode

We use Typescript as a glue for our Javascript code, we don’t want to reimplement the shape of the defaultState into an Interface by hand, because we trust our Javascript implementation. We will infer the type directly from the Javascript object.

const defaultState = {
  value: 10
}
type State = typeof defaultState
Enter fullscreen mode Exit fullscreen mode

Alt Text

As you can see it’s no big deal to infer a static type for the whole reducer state by using a single typeof keyword. There is a bottleneck if a default value does not describe the whole data type and Typescript can’t infer it correctly. For example an empty array. If you write an empty array you have no idea what data types will be inside of the array. For this kind of case, we will help the typescript-compiler by using the as keyword for specifying the type correctly as in the example below.

const defaultState = {
  users: [] as User[],
  admins: [] as User[],
}
type State = typeof defaultState
Enter fullscreen mode Exit fullscreen mode

Reducer

Reducer is a pure function that takes state and action and returns a new updated state. Basic Javascript implementation is just function with oneswitch caseas in the example.

function counter(state = defaultState, action) {
  switch (action.type) {
    case MULTIPLY:
      return { ...state, value: state.value * action.multiplyBy }
    case DIVIDE:
      return { ...state, value: state.value / action.divideBy }
    default:
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode

Adding Typescript to the reducer is simple. We will just connect reducers arguments with already created Typescript inferred State type and an ActionType enum with all reducers actions.

Alt Text

You can see that switch-case pattern matching started to magically infer a data type from the return value of the action creator function.

Combine reducers

combineReducers is a function that connects all the reducers into one giant nested object that is used as a global state which is a source of truth for the whole application. We know that a reducer returns an app sub-state which we inferred via typeof from the default State. So we are able to take the return value of all reducers and combine them to get the state of the whole App. For example:

const reducers = {
  users: usersReducer,
  helpers: combineReducers({
    counter: counterReducer,
  }),
};
Enter fullscreen mode Exit fullscreen mode

We will infer the App state by combing all reducers and apply the GetStateFromReducers generic which merges all reducers sub-states. combineReducers can be nest so our type inferring should works recursively. Generic GetStateFromReducers is a small util type that recursively infer returns values of all nested reducers and combines them into the global type.

export type GetStateFromReducers<
  T extends ((...args: any[]) => any) | { [key: string]: any }
> = {
  [K in keyof T]: T[K] extends ((...args: any[]) => any)
    ? ReturnType<T[K]>
    : GetStateFromReducers<T[K]>
}
Enter fullscreen mode Exit fullscreen mode

Now we just apply our generic to the reducers object and infer the App state.

Alt Text

If you add a new reducer into the Javascript implementation, Typescript automatically infers a new global state. So there are no duplicates of writing interfaces and implementation because everything is automatically inferred.

Selectors

Redux selector is a small function that takes global Redux state and picks some sub-state from it.

const getCounterValue = (state: GlobalState) => state.helpers.counter.value
Enter fullscreen mode Exit fullscreen mode

Alt Text

Now we connect the created selector to the React component by the useSelector hook.

const counterValue = useSelector(getCounterValue)
Enter fullscreen mode Exit fullscreen mode

Alt Text

Typescript connections preview

When you inferred the whole Redux state from the real Javascript implementation you get extra Typescript code connections between selectors and reducers. You can check it in your favorite IDE (I use VSCode) just by clicking something like a command/CMD + mouse click to data-type and IDE should jump to the code definition. If you try to do it the newly created example, an IDE will be redirected directly to the core implementation.

export const UIApp = () => {
  const dispatch = useDispatch()
  return (
    <div>
      <button onClick={() => { dispatch(divide(4))}}>divide by 4</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

It means that type inferring is much more valuable than just type interfaces written by hand! And you get it because you did not create an extra layer of a data shape abstraction and you just infer connections between your functions and objects.

Dispatch action directly from the React Component

You already created all redux actions so we’re gonna connect them with React Component. In pure React Javascript, code will be similar to this one.

Alt Text

We use the useDispatch hook to get dispatch function. dispatch takes action object which is created by our action creators (later in this chapter you will find out that you can pass also redux-thunk function). We want to create a union type for all possible Redux actions. We already combined all reducers together by combineReducer. So we will just take a second argument (action) of all reducers and get a union type for all of them.

First of all, we have to define generic that flat layers of combine reducers and get only object values to the one large union type.

export type Get2NestedValuesAsUnion<T> =
  T extends ({ [s: string]: infer ValNest1 })
    ? ValNest1 extends ({ [s: string]: infer ValNest2 })
      ? ValNest2
      : ValNest1
    : T
Enter fullscreen mode Exit fullscreen mode

(Pay attention! This generic supports only two levels of combineReducers nesting ).

We define another generic which infer the second argument of a function.

export type GetActionsFromReducer<T> =
  T extends ((...args: any[]) => any)
    ? Parameters<T>[1]
    : never
Enter fullscreen mode Exit fullscreen mode

Now we apply both generics to the reducers object and get list of all possible actions.

Alt Text

*AllReduxActions union contains all users/ and count/ actions but this union type is too large to be in one screenshot

Magic right? That’s the power of Typescript generics!

The last step is to re-declare a global data type for react-redux library and connect created AllReduxActions type to the useDispatch hook.

To do that we have to create global.d.ts a file where we replace libraries definitions with our custom ones. In this file, we redeclare the scope of react-redux library and change the Typescript type of useDispatch. We redeclare react-redux types by using of declare module xxx { You can read more about adding types to different modules there:
https://www.typescriptlang.org/docs/handbook/modules.html#ambient-modules

import { AllReduxActions } from './App'
import { ThunkReturnType } from './reduxHelperTypes'

declare module 'react-redux' {
  type UnspecificReduxThunkAction = (...arg: any[]) => any
  export function useDispatch(): (arg: AllReduxActions | UnspecificReduxThunkAction) => Promise<any>
}
Enter fullscreen mode Exit fullscreen mode

In this global.d.ts we already added support for redux-thunk by ThunkReturnType generic which will be described in the next part of this article.

We already defined all necessary pieces and we’re able to use useDispatch with a correctly typed all actions argument.

Alt Text

*arg arguments contain all users/ and count/ actions but this union type is too large to be in one screenshot

Async actions with redux-thunk

The last missing thing from our Redux example is async action dispatching. For this article, we choose to use redux-thunk library because it’s a simple package that is heavily used in the whole Redux ecosystem.

Redux-thunk enables us to write a function that takes custom parameters and returns a new function with pointers to dispatch and getState functions that enable you to create async Redux work-flow. If you don’t know redux-thunk look at the documentation. https://github.com/reduxjs/redux-thunk

A basic Javascript redux-thunk async function example.

const delay = (timeout) => new Promise(resolve => setTimeout(resolve, timeout))

export const asyncValueChange = (timeout) =>
  async (dispatch, _getState) => {
    await delay(timeout)
    dispatch(multiply(2))
    await delay(timeout)
    await delay(timeout)
    dispatch(multiply(5))
    await delay(timeout)
    dispatch(divide(7))
  };
Enter fullscreen mode Exit fullscreen mode

Alt Text

It would be a lot of work to write types for each function argument. Because of that, we created another util generic calledThunkReturnType which adds static types for the whole thunk function. The definition is relatively simple.

import { GlobalState, AllReduxActions } from "./App"

export type ThunkReturnType<
  R = Promise<unknown> | unknown,
  ExtraArgument = any
> =(
  dispatch: <T = Promise<unknown> | unknown>(
    a: AllReduxActions | ThunkReturnType
  ) => T,
  getState: () => GlobalState,
  extraArgument: ExtraArgument
) => R 
Enter fullscreen mode Exit fullscreen mode

Our final async thunk function is almost the same as the previous one written in pure Javascript. We just add ThunkReturnType static type for the returned async function.

Alt Text

Now you connected Javascript React Redux App with 100% type-safe Typescript types.

What’s next? 🎉🎉

Well… That’s all!

You have a fully typed Redux application with almost minimum effort of writing types! Anytime you create a new actions/reducers/sub-state/etc… almost all data-types and data-connections are automatically inferred and your code is type-safe, analyzable, and well self-documented.

The full type-safe React Redux app GitHub repo: https://github.com/Svehla/redux-ts-preview

Conclusion

We learned how to use advanced Typescript types and skip redundant static-type definitions. We used Typescript as a static compile-time type checker which infer types from Javascript business logic implementation. In our Redux example, we logically merged reducers with actions, combined-reducers with state and state with selectors. And the top of that, we support to dispatch async actions via the redux-thunks library.

In the diagram below we can see that all functions related to Redux have statically analyzed connections with the rest of the code. And we can use that feature to make consistent APIs between objects and redux functions.

Alt Text

Diagram Legend:
Blue lines — Typescript— **the connections “glue” of functions and objects

I hope that you have read all 3 parts of this series and you slightly changed your mindset on how to write static types in the Javascript ecosystem with the help of awesome tools which Typescript provides to us.

Do you disagree with these Articles? Don’t be afraid to start a conversation below. 💪

You can find the full redux-typescript app in this repository:
https://github.com/Svehla/redux-ts-preview

If you enjoyed reading the article don’t forget to like it.

Discussion (0)

pic
Editor guide