-
-
Save wonderful-panda/46c072497f8731a2bde28da40e9ea2d7 to your computer and use it in GitHub Desktop.
// 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)); |
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); | |
} | |
} | |
}); |
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 |
How to strictly type context.rootState
and context.rootGetters
in actions for createModule<S,M,A,G>
without having to write a bunch of repeated type parameters in the handler functions to impose an assumed type???
actions: {
// Check method names and infer payload types
["mod1/increment"](context:ActionContext<ModuleState,ModuleMutations,ModuleActions,ModuleGetters,State,StateGetters>, _) {
// context.getters.value_mod1;
// First parameter must be "mod1/setValue". Second must be number. (Compiler checks them)
context.commit("mod1/setValue", context.state.value + 1);
},
Also, when writing module.ts.
, it appears impossible for rootState
within a Module to be able to determine (in a strictly typed manner), any other registered module states under that given assumed rootState
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 of SMAG
, 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 include RS
param after SMAG
?, so that the final createStore
may validate against any RS
) that was set on builder and ensure S
matches the RS
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's getters
parameter in Getter or Action context handlers always refer to root namespace anyway, any manually specified "getters" types (eg. the G
in SMAG
or the RG
), 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.
How would a module1 action be able to respond to "increment" as well from root store? Do I manuallyinclude it in with & operator?
Take note of the minor caveat above. This is caused due to you relying on
&
operator for , which if commented away, will ensure strict typings only to given keyofs, and will not allow extra properties. You might need to re-factor it though to get it to compile again.Here's what i attempted with standalone MutationTree without applying
& Vuex.MutationTree<S>
.