Created
February 18, 2019 21:05
-
-
Save 3mcd/65f3a7ab3b5c75b2220a8d7621006734 to your computer and use it in GitHub Desktop.
Typescript Redux Modules
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 { 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