Last active
September 10, 2020 03:26
-
-
Save wuzzeb/e8502a6efb4160fa1c20ddf4080968c2 to your computer and use it in GitHub Desktop.
Type-safe react and redux actions
This file contains 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 * 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