Typescript and Redux. My tips.
Maksim
Posted on November 21, 2019
Introduction
Hello everybody!
Today I want to talk about quite popular technologies. Typescript and Redux. Both helps to develop fault tolerant applications. There is a lot of approaches to write typings of state and actions. I formed own, that could save your time.
State
Each state in Redux should be immutable. Immutable object cannot be modified after it is created. If you forget this rule, your component do not rerender after state changes. So let's use Readonly
modifier. It makes all properties readonly. You can't mutate property in reducer.
export type State = Readonly<{
value: number;
}>
Do not forget use Readonly
modifier for nested objects too. But what about arrays. For example:
export type State = Readonly<{
list: number[];
}>
You still can change it. Let's fix it, TypeScript includes special modifier ReadonlyArray
.
export type State = Readonly<{
list: ReadonlyArray<number>;
}>
Now you can't add or remove items. You have to create new array for changes. Also TypeScript has special modifiers for Map and Set: ReadonlyMap
and ReadonlySet
.
Actions
I use enums for Redux actions. Naming convention is simple: @namespace/effect
. Effect always in past tense, because it is something already happened. For example, @users/RequestSent
, @users/ResponseReceived
, @users/RequestFailed
...
enum Action {
ValueChanged = '@counter/ValueChanged',
}
Action creators
Little magic starts.
The first thing, we use const assertions. The const assertion allowed TypeScript to take the most specific type of the expression.
The second thing, we extract return types of action creators by type inference.
const actions = {
setValue(value: number) {
return {
payload: value,
type: Action.ValueChanged,
} as const;
},
}
type InferValueTypes<T> = T extends { [key: string]: infer U } ? U : never;
type Actions = ReturnType<InferValueTypes<typeof actions>>;
Let's improve it by helper function:
export function createAction<T extends string>(
type: T,
): () => Readonly<{ type: T }>;
export function createAction<T extends string, P>(
type: T,
): (payload: P) => Readonly<{ payload: P; type: T }>;
export function createAction<T extends string, P>(type: T) {
return (payload?: P) =>
typeof payload === 'undefined' ? { type } : { payload, type };
}
Then our actions object will look:
const actions = {
setValue: createAction<Action.ValueChanged, number>(Action.ValueChanged)
}
Reducers
Inside reducer we just use things described before.
const DEFAULT_STATE: State = 0;
function reducer(state = DEFAULT_STATE, action: Actions): State {
if (action.type === Action.ValueChanged) {
return action.payload;
}
return state;
}
Now, for all of your critical changes inside action creators, TypeScript throws error inside reducer. You will have to change your code for correct handlers.
Module
Each module exports object like this:
export const Module = {
actions,
defaultState: DEFAULT_STATE,
reducer,
}
You can also describe your saga inside module, if you use redux-saga
.
Configure store
Describe the whole state of application, all actions and store.
import { Store } from 'redux';
type AppState = ModuleOneState | ModuleTwoState;
type AppActions = ModuleOneActions | ModuleTwoActions;
type AppStore = Store<AppState, AppActions>;
Hooks
If you use hooks from react-redux
, it would be helpful too.
By default you need to describe typings each time, when you use this hooks. Better to make it one time.
export function useAppDispatch() {
return useDispatch<Dispatch<AppActions>>();
}
export function useAppSelector<Selected>(
selector: (state: AppState) => Selected,
equalityFn?: (left: Selected, right: Selected) => boolean,
) {
return useSelector<AppState, Selected>(selector, equalityFn);
}
Now you can't dispatch invalid action.
The end
I hope all of this things will make your live easier.
I will glad for your comments and questions.
My twitter.
Posted on November 21, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.