Skip to content

Instantly share code, notes, and snippets.

@3mcd
Created February 18, 2019 21:05
Show Gist options
  • Save 3mcd/65f3a7ab3b5c75b2220a8d7621006734 to your computer and use it in GitHub Desktop.
Save 3mcd/65f3a7ab3b5c75b2220a8d7621006734 to your computer and use it in GitHub Desktop.
Typescript Redux Modules
import { produce } from "immer";
type Reducer<S, A> = (state: S, action: A) => S;
interface ISelectorMap<S> {
[key: string]: (state: S, ...args: any[]) => any;
}
interface IActionCreatorMap {
[actionType: string]: (...args: any) => any;
}
interface IModuleConfig<
S,
AC extends IActionCreatorMap,
SC extends ISelectorMap<S>
> {
actions: AC;
reducer: Reducer<S, LiftActions<AC>>;
selectors: SC;
}
export type ReduxModule<
N extends string,
State,
AC extends IActionCreatorMap,
SC extends ISelectorMap<State>
> = Reducer<State, LiftActions<AC>> &
MapActionCreatorTypes<AC> &
MapSelectors<N, State, SC>;
type ExtractSelectorArguments<T> = T extends (
arg0: any,
...args: infer A
) => any
? A
: never;
type LiftActions<
AC extends IActionCreatorMap,
Keys extends string = Extract<keyof AC, string>
> = { [K in Keys]: { type: K; payload: ReturnType<AC[K]> } }[Keys];
type ExtractArguments<T> = T extends (...args: infer A) => any ? A : never;
type MapActionCreatorTypes<AC extends IActionCreatorMap> = {
[T in keyof AC]: (
...args: ExtractArguments<AC[T]>
) => { type: T; payload: ReturnType<AC[T]> }
};
type MapSelectors<N extends string, State, SC extends ISelectorMap<State>> = {
[K in keyof SC]: (
state: { [$N in N]: State },
...args: ExtractSelectorArguments<SC[K]>
) => ReturnType<SC[K]>
};
export function createReduxModule<
N extends string,
S,
AC extends IActionCreatorMap,
SC extends ISelectorMap<S>
>(
reduxModuleName: N,
initialState: S,
config: IModuleConfig<S, AC, SC>,
): ReduxModule<N, S, AC, SC> {
const withScope = (key: string) => `${reduxModuleName}/${key}`;
function reducer(state: S = initialState, action: LiftActions<AC>) {
return produce(state, draft => {
config.reducer(draft, {
...action,
type: action.type.replace(`${reduxModuleName}/`, ""),
} as any);
});
}
const actionCreators = Object.entries(config.actions).reduce(
(actionCreators, [actionCreatorExportName, actionCreator]) => {
const wrappedActionCreator = (...args: any[]) => {
const result = actionCreator(...args);
return result instanceof Promise // handle thunks
? result
: {
type: withScope(actionCreatorExportName),
payload: result,
};
};
return {
...actionCreators,
[actionCreatorExportName]: wrappedActionCreator,
};
},
{} as AC,
);
const selectors = Object.entries(config.selectors).reduce(
(selectors, [selectorExportName, selector]) => {
const wrappedSelector = (state: any, ...args: any[]) => {
return selector(state[reduxModuleName], ...args);
};
return { ...selectors, [selectorExportName]: wrappedSelector };
},
{} as SC,
);
return Object.assign(reducer, actionCreators, selectors);
}
export type ExtractModuleActions<T> = T extends ReduxModule<
any,
any,
infer AC,
any
>
? LiftActions<AC>
: void;
export type ExtractModuleMapActions<
M,
Keys extends string = Extract<keyof M, string>
> = M extends {
[moduleName: string]: ReduxModule<string, any, infer AC, any>;
}
? { [K in Keys]: ExtractModuleActions<M[K]> }[Keys]
: void;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment