Skip to content

Instantly share code, notes, and snippets.

@starstuck
Last active April 18, 2019 17:24
Show Gist options
  • Save starstuck/719458285ea0c14a48a81ea9c5c2ebe1 to your computer and use it in GitHub Desktop.
Save starstuck/719458285ea0c14a48a81ea9c5c2ebe1 to your computer and use it in GitHub Desktop.
Concise, strong typing in Redux actions and reducers (TypeScript).
import { ActionCreatorsMapObject, AnyAction } from 'redux';
export enum ActionType {
ReportError = 'REPORT_ERROR',
UpdateToDos = 'UPDATE_TODOS'
}
type ThunkDispatch = <T extends { type: ActionType }>(action: T) => T;
type ThunkAction<A extends { type: ActionType }, S, E> = (
dispatch: ThunkDispatch,
getState: () => S,
extraArgument: E,
) => Promise<A>;
type PlainCreator<T extends AnyAction> = (...args: any[]) => T;
type ThunkCreator<T extends AnyAction> = (
...args: any[]
) => ThunkAction<T, any, any> | Promise<ThunkAction<T, any, any>>;
type AnyCreator = PlainCreator<AnyAction> | ThunkCreator<AnyAction>;
type CreatorActionType<T> = T extends PlainCreator<infer A> ? A : T extends ThunkCreator<infer B> ? B : never;
type AllCreatorNames<T extends ActionCreatorsMapObject> = {
[K in keyof T]: T[K] extends AnyCreator ? K : never
}[keyof T];
type AllCreators<T extends ActionCreatorsMapObject> = T[AllCreatorNames<T>];
export type ActionBy<T extends ActionCreatorsMapObject> = CreatorActionType<AllCreators<T>>;
// for action creators: disables widening of 'type' property
export function createAction<T extends { type: ActionType }>(d: T) {
return d;
}
// similar for thunk creators
export function createThunk<T extends { type: ActionType }, S, E>(c: ThunkAction<T, S, E>) {
return c;
}
// --- Action crators ---
export const reportError = (error: Error) =>
createAction({
type: ActionType.ReportError,
error,
});
export const fetchTodos= () =>
createThunk(async (dispatch, _, { api }: ExtraArg) => {
return api.listTodos().then(todos =>
dispatch({
type: ActionType.UpdateTodos,
todos,
}),
);
});
import { createStore, applyMiddleware, Middleware } from 'redux';
import thunk from 'redux-thunk';
import { reducer, StoreState, Action } from './reducers';
import Api from './api';
export function create(initialState?: StoreState, extraMiddlewares: Middleware[] = []) {
const api = new ApiClient();
const thunkMiddleware = thunk.withExtraArgument({ api });
const enhancer = applyMiddleware(thunkMiddleware, ...extraMiddlewares);
const store = createStore<StoreState, Action, {}, {}>(reducer, initialState, enhancer);
return store;
}
import { combineReducers, Reducer } from 'redux';
import { TodoItem } from './api';
import { ActionType, ActionBy } from './actions';
import * as actionCreators from './actions';
type Action = ActionBy<typeof actionCrators>;
const todoDefaultState = {
items: [] as ReadonlyArray<TodoItem>
}
const todoReducer = (state = todoDefaultState, action: Action) {
switch (action.type) {
case ActionType.updateToDos:
// Typescript will be able to know attributes for *exact* event type
const { todos } = action;
default:
return state;
}
}
const appDefaultState = {
// ...
}
const appReducer = (state = appDefaultState, action: Action) {
// ...
}
// --- Combine reducers and store state type information ---
type ReducerAction<T> = T extends Reducer<any, infer A> ? A : never;
type ReducerState<T> = T extends Reducer<infer A, any> ? A : never;
export type Action = ReducerAction<typeof app> | ReducerAction<typeof explorer>;
export type StoreState = {
app: ReducerState<typeof app>;
explorer: ReducerState<typeof explorer>;
dictionaries: ReducerState<typeof dictionaries>;
};
export const reducer = combineReducers<StoreState, Action>({
app,
todos,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment