Skip to content

Instantly share code, notes, and snippets.

@wuzzeb
Last active September 10, 2020 03:26
Show Gist options
  • Save wuzzeb/e8502a6efb4160fa1c20ddf4080968c2 to your computer and use it in GitHub Desktop.
Save wuzzeb/e8502a6efb4160fa1c20ddf4080968c2 to your computer and use it in GitHub Desktop.
Type-safe react and redux actions
import * as React from "react";
import * as redux from "redux";
import * as reactRedux from "react-redux";
// a variant of middleware which tracks input and output types of the actions
export type Dispatch<A> = (a: A) => void;
export type Middleware<A1, A2> = (dispatch: Dispatch<A2>) => (a: A1) => void;
export function composeMiddleware<A1, A2, A3>(m1: Middleware<A1, A2>, m2: Middleware<A2, A3>): Middleware<A1, A3> {
return d => m1(m2(d));
}
// Extract the state and action types from the type of an object of reducer functions.
// For example, Reducers will be the type of an object literal such as
// {
// foo: fooReducer,
// bar: barReducer
// }
// where fooReducer has type (s: FooState, a: FooAction) => FooState
// and barReducer has type (s: BarState, a: BarAction) => BarState.
// ReducerFnsToState will translate this to a type
// {
// foo: FooState,
// bar: BarState
// }
// ReducerFnsToActions will translate this to a union type FooAction | BarAction
type ReducerFnsToState<Reducers> = {
readonly [K in keyof Reducers]: Reducers[K] extends ((s: infer S, a: infer A) => infer S)
? (S extends (infer S2 | undefined) ? S2 : S)
: never;
};
type ReducerFnsToActions<Reducers> = {
[K in keyof Reducers]: Reducers[K] extends ((s: infer S, a: infer A) => infer S) ? A : never;
}[keyof Reducers];
// extract the type and payload of a union of action types.
// (The union of action types is produced by ReducerFnsToActions above.)
type RemoveTypeProp<P> = P extends "type" ? never : P;
type RemoveType<A> = { [P in RemoveTypeProp<keyof A>]: A[P] };
type ActionTypes<A> = A extends { type: infer T } ? T : never;
type Payload<A, T> = A extends { type: T } ? RemoveType<A> : never;
type DispatchAction<ActionBeforeMiddleware, T> = {} extends Payload<ActionBeforeMiddleware, T>
? () => void
: (payload: Payload<ActionBeforeMiddleware, T>) => void;
function useDispatch<ActionBeforeMiddleware, T extends ActionTypes<ActionBeforeMiddleware>>(
ty: T
): DispatchAction<ActionBeforeMiddleware, T> {
const dispatch = reactRedux.useDispatch();
return React.useCallback(
(payload: any) => {
if (payload) {
dispatch({ ...payload, type: ty });
} else {
dispatch({ type: ty });
}
},
[dispatch, ty]
) as any;
}
// The react-redux store with the state and action types (which will be infered from the reducers).
export interface Store<ActionBeforeMiddleware, State> {
readonly Provider: React.ComponentType<{ children: React.ReactNode }>;
readonly useSelector: reactRedux.TypedUseSelectorHook<State>;
useDispatch<T extends ActionTypes<ActionBeforeMiddleware>>(ty: T): DispatchAction<ActionBeforeMiddleware, T>;
dispatch(a: ActionBeforeMiddleware): void;
getState(): Readonly<State>;
subscribe(listener: () => void): redux.Unsubscribe;
}
export type StoreActions<S> = S extends Store<infer Act, infer State> ? Act : never;
export type StoreState<S> = S extends Store<infer Act, infer State> ? State : never;
export function createStore<Reducers, ActionBeforeMiddleware>(
reducers: Reducers,
middleware: Middleware<ActionBeforeMiddleware, ReducerFnsToActions<Reducers>>,
middlewareToEnhancer?: (m: redux.Middleware) => redux.StoreEnhancer
): Store<ActionBeforeMiddleware, ReducerFnsToState<Reducers>> {
// tslint:disable-next-line:no-any
const reduxMiddleware = (_store: any) => middleware as any;
const st = redux.createStore(
redux.combineReducers(reducers),
(middlewareToEnhancer || redux.applyMiddleware)(reduxMiddleware)
);
return {
useSelector: reactRedux.useSelector,
useDispatch: useDispatch,
// tslint:disable-next-line:no-any
dispatch: st.dispatch as any,
// tslint:disable-next-line:no-any
getState: st.getState as any,
subscribe: st.subscribe,
Provider: ({ children }: { children: React.ReactNode }) =>
React.createElement(reactRedux.Provider, { store: st }, children)
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment