Redux withTypeScript — the proper way to strong typing consistent with Redux Toolkit

Michal Jurkowski
3 min readApr 17, 2020

Everyone how use TypeScript see the huge value from strong typing — it’s secure and predictable. I saw a lot of projects that use current versions of Redux, and they still have a lot of self declarative interfaces. That is a huge problem if something change…

Before we start I recommend to install Redux Toolkit using npm install --save @reduxjs/toolkit that includes a lot of fancy creators that I think you fall in love!

Step 1. Actions

Redux Toolkit provide us a method createAction which is action creator of type and optional payload. I like to have separated payload in action as independent key, so let’s start with that helper

// helpers.tsimport { createAction } from '@reduxjs/toolkit';function withPayloadType<T>() {
return (t: T) => ({ payload: t });
}
export const createActionWithPayload = <T>(action: string) =>
createAction(action, withPayloadType<T>());
export { createAction };

Then we have two separate methods for action creators — with or without payload.

So, let begin making our actions. We create an Enum for action names, but the final result is a const that looks like enum but it’s key-function object with our actions that are ready to dispatch.

// actions.tsimport { createActionWithPayload, createAction } from './helpers';
import { ToDo, ToDoID } from './types';
enum ToDoActionName {
ADD_TODO = 'ADD_TODO',
REMOVE_TODO = 'REMOVE_TODO'
}
export const ToDoActions = {
add: createActionWithPayload<ToDo>(ToDoActionName.ADD_TODO),
remove: createActionWithPayload<ToDoID>(ToDoActionName.REMOVE_TODO)
);

Step 2. Reducer

That’s the essence of the Redux Toolkit — createReducer with builder that based on the initial state type, and using the addCase method provide us the types of the state and action.

// reducer.tsimport { createReducer } from '@reduxjs/toolkit';import { ToDo, ToDoID } from './types';
import { ToDoActions } from './actions';
interface ToDoState extends Record<ToDoID, ToDo> {}const initialState: ToDoState = {};export const toDoReducer = createReducer(initialState, builder =>
builder
.addCase(ToDoActions.add, (state, { payload }) => ({
...state,
[payload.id]: payload
}))
.addCase(ToDoActions.remove, (state, { payload }) => {
const newState = { ...state };
delete newState[payload];
return newState;
})
);

Step 3. Store

Here we have 3 things to do:

  1. Create a store
  2. Get the store state type
  3. Add some enhancers like redux devtools
// store.tsimport { createStore, combineReducers, compose } from 'redux';import { toDoReducer } from './reducer';const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;const reducer = combineReducers({
todos: toDoReducer
});
export type StoreState = ReturnType<typeof reducer>export const store = createStore(reducer, composeEnhancers());

Step 4. Selectors

And right now we have easy situation of the type checking — we have dynamic type of StoreState so it will be very easy to create new selectors if you put new reducer, or if you change current reducer.

// selectors.tsimport { StoreState } from './store';
import { ToDo, ToDoId } from './types';
export const getToDo = (state: StoreState, id: ToDoId): ToDo => state.todos[id];

Summary

As you can see, it’s very easy, and right now you can be sure, that all types are very strong related with each other, so each type change impact will be very easy to recognize.

--

--