Last active
April 18, 2019 17:24
-
-
Save starstuck/719458285ea0c14a48a81ea9c5c2ebe1 to your computer and use it in GitHub Desktop.
Concise, strong typing in Redux actions and reducers (TypeScript).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}), | |
); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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