
Redux withTypeScript — the proper way to strong typing consistent with Redux Toolkit
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:
- Create a store
- Get the store state type
- 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.