Last active
March 18, 2018 19:22
-
-
Save wonderful-panda/46c072497f8731a2bde28da40e9ea2d7 to your computer and use it in GitHub Desktop.
vuex store builder (TypeScript >= 2.1 is required / namespaced module is not supported)
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
// MIT License | |
import * as Vuex from "vuex"; | |
/* | |
* Infrastructure types | |
*/ | |
export type KV<K extends string, V> = { [_ in K]: V }; | |
export type Payload<T, V> = { type: T } & V; | |
/* | |
* Extends vuex types with additional type parameters. | |
*/ | |
export interface Dispatch<A> { | |
<K extends keyof A>(payload: Payload<K, A[K]>, options?: Vuex.DispatchOptions): Promise<any[]>; | |
<K extends keyof A>(type: K, payload: A[K], options?: Vuex.DispatchOptions): Promise<any[]>; | |
} | |
export interface Commit<M> { | |
<K extends keyof M>(payload: Payload<K, M[K]>, options?: Vuex.CommitOptions): void; | |
<K extends keyof M>(type: K, payload: M[K], options?: Vuex.CommitOptions): void; | |
} | |
export interface ActionContext<S, M, A, G, RS> extends Vuex.ActionContext<S, RS> { | |
dispatch: Dispatch<A>; | |
commit: Commit<M>; | |
getters: G; | |
} | |
export type MutationTree<S, NewMutations> = { | |
[K in keyof NewMutations]: (state: S, payload: NewMutations[K]) => any | |
} & Vuex.MutationTree<S>; | |
export type ActionTree<S, M, A, G, NewActions, RS> = { | |
[K in keyof NewActions]: (injectee: ActionContext<S, M, A & NewActions, G, RS>, payload: NewActions[K]) => any | |
} & Vuex.ActionTree<S, RS>; | |
export type GetterTree<S, G, NewGetters, RS, RG> = { | |
[K in keyof NewGetters]: (state: S, getters: G & NewGetters, rootState: RS, rootGetters: RG) => NewGetters[K]; | |
} & Vuex.GetterTree<S, RS>; | |
/** | |
* Vuex module with type information | |
*/ | |
export interface CombinedModule<S, M, A, G, RS, RG> extends Vuex.Module<S, RS> { | |
// dummy member let this interface behave as phantom type | |
__types?: { | |
s: S, m: M, a: A, g: G, rs: RS, rg: RG | |
}; | |
} | |
export interface Module<S, M, A, G, RS, RG, CS extends S, CM extends M, CA extends A, CG extends G> extends Vuex.Module<S, RS> { | |
mutations?: MutationTree<CS, M>; | |
actions?: ActionTree<CS, CM, CA, CG, A, RS>; | |
getters?: GetterTree<CS, CG, G, RS, RG>; | |
} | |
export interface StoreOptions<S, M, A, G, | |
CombinedState extends S, | |
CombinedMutations extends M, | |
CombinedActions extends A, | |
CombinedGetters extends G> extends Vuex.StoreOptions<S> { | |
mutations?: MutationTree<CombinedState, M>; | |
actions?: ActionTree<CombinedState, CombinedMutations, CombinedActions, CombinedGetters, A, CombinedState>; | |
getters?: GetterTree<CombinedState, CombinedGetters, G, CombinedState, CombinedGetters>; | |
} | |
export interface Store<S, M, A, G> extends Vuex.Store<S> { | |
dispatch: Dispatch<A>; | |
commit: Commit<M>; | |
readonly getters: G; | |
} | |
/** | |
* Vuex store / module builder | |
* | |
* @param <CS> combined state (intersection of sub modules' state) | |
* @param <CM> combined mutations (intersection of sub modules' mutations) | |
* @param <CA> combined actions (intersection of sub modules' actions) | |
* @param <CG> combined getters (intersection of sub modules' getters) | |
* @param <CRS> combined root state (intersection of sub modules' rootstate) | |
* @param <CRG> combined root getters (intersection of sub modules' rootgetters) | |
*/ | |
export class Builder<CS, CM, CA, CG, CRS, CRG> { | |
constructor(private readonly modules: { [name: string]: CombinedModule<any, any, any, any, any, any> }) { | |
} | |
/** | |
* Add module to the builder. | |
* Added modules will be involved to store as submodules. | |
*/ | |
addModule<S, M, A, G, RS, RG, N extends string>(name: N, m: CombinedModule<S, M, A, G, RS, RG>): | |
Builder<CS & KV<N, S>, CM & M, CA & A, CG & G, CRS & RS, CRG & RG> { | |
const newModules = Object.assign({}, this.modules, { [<string>name]: m }); | |
return new Builder<CS & KV<N, S>, CM & M, CA & A, CG & G, CRS & RS, CRG & RG>(newModules); | |
} | |
/** | |
* Specify types of root state and root getters | |
* new types must be extended from current types | |
*/ | |
rootTypes<RootState extends CRS, RootGetters extends CRG>(): Builder<CS, CM, CA, CG, CRS, CRG> { | |
return this; | |
} | |
/** | |
* create store instance from specified store options and previously added submodules. | |
* | |
* @param <S> type of state | |
* @param <M> type of mutations (optional) | |
* @param <A> type of actions (optional) | |
* @param <G> type of getters (optional) | |
* @params options store options | |
*/ | |
createStore<S extends CRS>(options: StoreOptions<S, {}, {}, {}, CS & S, CM, CA, CG>): Store<CS & S, CM, CA, CG>; | |
createStore<S extends CRS, M>(options: StoreOptions<S, M, {}, {}, CS & S, CM & M, CA, CG>): Store<CS & S, CM & M, CA, CG>; | |
createStore<S extends CRS, M, A>(options: StoreOptions<S, M, A, {}, CS & S, CM & M, CA & A, CG>): Store<CS & S, CM & M, CA & A, CG>; | |
createStore<S extends CRS, M, A, G>(options: StoreOptions<S, M, A, G, CS & S, CM & M, CA & A, CG & G>): Store<CS & S, CM & M, CA & A, CG & G>; | |
createStore(options: Vuex.StoreOptions<any>) { | |
let { modules, ...rest } = options; | |
modules = Object.assign(Object.create(null), modules, this.modules); | |
return new Vuex.Store(Object.assign(rest, { modules })); | |
} | |
/** | |
* create combined module from specified modules and previously added submodules. | |
* | |
* @param <S> type of state | |
* @param <M> type of mutations (optional) | |
* @param <A> type of actions (optional) | |
* @param <G> type of getters (optional) | |
* @params options store options | |
*/ | |
createModule<S>(m: Module<S, {}, {}, {}, CRS, CRG, CS & S, CM, CA, CG>): CombinedModule<CS & S, CM, CA, CG, CRS, CRG>; | |
createModule<S, M>(m: Module<S, M, {}, {}, CRS, CRG, CS & S, CM & M, CA, CG>): CombinedModule<CS & S, CM & M, CA, CG, CRS, CRG>; | |
createModule<S, M, A>(m: Module<S, M, A, {}, CRS, CRG, CS & S, CM & M, CA & A, CG>): CombinedModule<CS & S, CM & M, CA & A, CG, CRS, CRG>; | |
createModule<S, M, A, G>(m: Module<S, M, A, G, CRS, CRG, CS & S, CM & M, CA & A, CG & G>): CombinedModule<CS & S, CM & M, CA & A, CG & G, CRS, CRG>; | |
createModule(m: Vuex.Module<any, any>) { | |
let { modules, ...rest } = m; | |
modules = Object.assign(Object.create(null), modules, this.modules); | |
return Object.assign({ modules }, rest); | |
} | |
} | |
/** | |
* Default builder instance | |
*/ | |
export const builder = new Builder(Object.create(null)); |
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 { builder } from "./builder"; | |
interface ModuleState { | |
value: number; | |
} | |
interface ModuleMutations { | |
"mod1/setValue": number; | |
} | |
interface ModuleActions { | |
"mod1/increment": undefined; | |
} | |
export default builder.createModule<ModuleState, ModuleMutations, ModuleActions>({ | |
state: { value: 1 }, | |
mutations: { | |
// Check method names and infer payload types | |
["mod1/setValue"](state, value) { // state is inferred as ModuleState, value is inferred as number. | |
state.value = value; | |
} | |
}, | |
actions: { | |
// Check method names and infer payload types | |
["mod1/increment"](context, _) { | |
// First parameter must be "mod1/setValue". Second must be number. (Compiler checks them) | |
context.commit("mod1/setValue", context.state.value + 1); | |
} | |
} | |
}); |
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 { builder } from "./builder"; | |
import module1 from "./module1"; | |
interface State { | |
root: number; | |
} | |
interface Mutations { | |
"setRoot": number; | |
} | |
interface Actions { | |
"increment": undefined; | |
} | |
const store = builder.addModule( | |
// corresponding to { modules: { child: module1 } } | |
// this will intersect types from module1, and make enable to call mutations/actions of module1 | |
"child", module1 | |
).createStore<State, Mutations, Actions>({ | |
state: { root: 1 }, | |
mutations: { | |
setRoot(state, value) { // type of state will be { root: number, child: { value: number } } (State & { child: ModuleState }) | |
state.root = value; | |
} | |
}, | |
actions: { | |
increment(context, _) { | |
context.commit("setRoot", context.state.root + 1); | |
context.dispatch("mod1/increment", undefined); // module1 actions can be dispatched here. | |
} | |
} | |
}); | |
store.commit("setRoot", 1); // OK | |
store.commit("mod1/setValue", 1); // OK | |
store.commit("setRoot", "foo"); // Compile error | |
store.commit("mod1/setValue", "foo"); // Compile error | |
store.commit("setRot", 1); // Compile error | |
store.dispatch("increment", undefined); // OK | |
store.dispatch("mod1/increment", undefined); // OK | |
store.dispatch("increment", 1); // Compile error | |
store.dispatch("incrment", undefined); // Compile error |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How to strictly type
context.rootState
andcontext.rootGetters
in actions forcreateModule<S,M,A,G>
without having to write a bunch of repeated type parameters in the handler functions to impose an assumed type???Also, when writing
module.ts.
, it appears impossible forrootState
within a Module to be able to determine (in a strictly typed manner), any other registered module states under that given assumedrootState
type. Normally, rootState should also contain the combined state of all other modules that may be added in the future. However, since a module is created first before the store with only 4 parameters ofSMAG
, there's no way a module can effectively predict what other modules under the given root state type it needs to interact with.Also, shouldn't
createModule
also includeRS
param afterSMAG
?, so that the finalcreateStore
may validate against anyRS
) that was set on builder and ensureS
matches theRS
found on previously added modules?. Not sure if it's possible to do in Typescript.The same issue occurs likewise for
rootGetters
, since there might be other modules' getters registered in the future to global root namespace as well which aren't accounted for within a module. Also, since a non-namespaced module'sgetters
parameter in Getter or Action context handlers always refer to root namespace anyway, any manually specified "getters" types (eg. theG
inSMAG
or theRG
), won't be "truly accruate" as well, since it refers to global namespace as well (ie. it won't reflect the "full actual state" of fields found on global namespace) Thus, it would seem as far as root getters are concerned, dangerous assumptions over what root getter fields are dynamically available or not may end up being manually imposed, which isn't good.