Skip to content

Instantly share code, notes, and snippets.

@kadet1090
Last active May 23, 2021 13:08
Show Gist options
  • Save kadet1090/6f204e0265f6a258579b1e92772158c8 to your computer and use it in GitHub Desktop.
Save kadet1090/6f204e0265f6a258579b1e92772158c8 to your computer and use it in GitHub Desktop.

New Document# Strong types for Vuex

Proof of concept for usage of [template literal types] for vuex stores and modules. For now it supports nested (namespaced and not) modules and checking mutation (name, payload) pairs but for now allows only argument style commit function.

TODO

  • Modules
    • Global VuexGlobalModule<TState, TMutations = {}, TActions = {}, TModules = {}, TGetters = {}>
    • Namespaced VuexNamespacedModule<TState, TMutations = {}, TActions = {}, TModules = {}, TGetters = {}>
    • State TState
      • State helper VuexState<TModule>
        • Own VuexOwnState<TModule>
        • From submodules
    • Mutations TMutations extends VuexMutationsTree
      • Non-type-safe fallback VuexMutationsTree ??
      • Available mutations VuexMutations<TModule>
        • Own VuexOwnMutations<TModule>
        • From submodules
          • Global
          • Namespaced
      • Mutation handler type VuexMutationHandler<TState, TPayload = never>
        • Properly type this in handler (store backref)
      • Commit types
        • Payload VuexMutationPayload<TModule, TMutation> Type is too deep
        • Argument-Style VuexCommit<TModule>
        • Object-Style VuexMutations<TModule>
        • Commit options VuexCommitOptions
          • Support { root: true }
    • Actions TActions extends VuexActionsTree
      • Non-type-safe fallback VuexActionsTree ??
      • Available actions VuexActions<TModule>
        • Own VuexOwnActions<TModule>
        • From submodules
          • Global
          • Namespaced
      • Action handler VuexActionHandler<TState, TPayload = never, TResult = Promise<void>>
        • Action Context VuexActionContext<TModule, TStoreDefinition = any>
        • Properly type this in handler (store backref)
      • Dispatch type VuexDispatch<TModule>
        • Payload VuexActionPayload<TModule, TAction>
        • Result VuexActionResult<TModule, TAction>
        • Argument-Style
        • Object-Style VuexAction<TModule>
        • Dispatch Options VuexDispatchOptions
          • Support { root: true }
    • Getters TGetters extends VuexGettersTree
      • Non-type-safe fallback VuexGettersTree ??
      • Available getters VuexGetters<TModule>
        • Own VuexOwnGetters<TModule>
        • From submodules
          • Global
          • Namespaced
      • Getter type VuexGetter<TState, TResult>
      • Result VuexGetterResult<TModule, TGetter>
    • Submodules TModules extends VuexModulesTree
  • Store Definition VuexStoreDefinition<TState, TMutations = {}, TActions = {}, TModules = {}, TGetters = {}, TPlugins = {}>
    • Basically VuexGlobalModule with additional things
    • Plugins VuexPlugin<TStoreDefinition>
    • Simple properties (devtools, etc.)
  • Store instance VuexStore<TStoreDefinition>
    • Constructor
      • Store Options VuexStoreOptions<TDefinition>
    • State (as defined by TStoreDefinition)
      • Replace state replaceState
    • Getters (as defined by TStoreDefinition)
    • Commit (as defined by TStoreDefinition)
    • Dispatch (as defined by TStoreDefinition)
    • Subscribers
      • Options SubscribeOptions
      • Actions subscribeAction
        • Subscriber VuexActionSubscriber<TDefinition>
          • Hook VuexActionHook<?>
          • Error VuexActionError<?>
          • Object VuexActionSubscribersObject<TDefinition>
      • Mutations subscribe
        • Subscriber VuexMutationSubscriber<TDefinition>
    • Watch watch
      • Options WatchOptions
    • Dynamic module management
      • Registration registerModule
      • Unregistration unregisterModule
      • Presence check hasModule
    • Hot Update ??

Typescript Playground (Demo)

click [template literal types]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#template-literal-types

type VuexModule<
TNamespaced extends boolean,
TMutations extends VuexMutationsTree,
TModules extends VuexModulesTree
> = {
mutations: TMutations;
modules: TModules,
} & (
TNamespaced extends true
? { namespaced: true }
: { namespaced?: false }
);
type VuexMutationHandler<TState, TPayload = never>
= [TPayload] extends [never]
? (state: TState) => void
: (state: TState, payload: TPayload) => void
;
type VuexMutationsTree
= {
[name: string]: VuexMutationHandler<any, any>;
}
;
type VuexModulesTree
= {
[name: string]: VuexModule<any, any, any>
}
;
type NamespacedVuexModule<
TMutations extends VuexMutationsTree,
TModules extends VuexModulesTree
> = VuexModule<true, TMutations, TModules>
;
type GlobalVuexModule<
TMutations extends VuexMutationsTree,
TModules extends VuexModulesTree
> = VuexModule<false, TMutations, TModules>
;
type VuexStoreDefinition<
TMutations extends VuexMutationsTree,
TModules extends VuexModulesTree
> = Omit<GlobalVuexModule<TMutations, TModules>, "namespaced">
& {
strict?: boolean,
devtools?
: boolean,
}
;
type UnionToIntersection<T>
= (T extends any ? (x: T) => any : never) extends (x: infer R) => any
? R
: never
type AddPrefix<TValue extends string, TPrefix extends string = never>
= [TPrefix] extends [never]
? TValue
: `${TPrefix}/${TValue}`
;
type VuexMutationHandlerPayload<TMutation extends VuexMutationHandler<any, any>>
= Parameters<TMutation>[1] extends undefined
? never
: Parameters<TMutation>[1]
;
export interface VuexCommitOptions {
silent?: boolean;
root?: boolean;
}
type VuexCommit<TMutation, TPayload>
= [TPayload] extends [never]
? (mutation: TMutation, payload?: undefined, options?: VuexCommitOptions) => void
: (mutation: TMutation, payload: TPayload, options?: VuexCommitOptions) => void
;
type VuexModuleOwnCommits<TModule extends VuexModule<any, any, any>, TPrefix extends string = never>
= UnionToIntersection<{
[TMutation in keyof TModule["mutations"]]: VuexCommit<
AddPrefix<string & TMutation, TPrefix>,
VuexMutationHandlerPayload<TModule["mutations"][TMutation]>
>;
}[keyof TModule["mutations"]]>
;
type VuexModuleCommit<TModule extends VuexModule<any, any, any>, TPrefix extends string = never>
= VuexModuleOwnCommits<TModule, TPrefix>
& VuexCommitOfModules<TModule["modules"], TPrefix>
;
type VuexCommitByModule<TModules extends VuexModulesTree, TPrefix extends string = never>
= {
[TKey in keyof TModules]:
VuexModuleCommit<
TModules[TKey],
AddPrefix<TModules[TKey] extends NamespacedVuexModule<any, any> ? (string & TKey) : never, TPrefix>
>
}
;
type VuexCommitOfModules<TModules extends VuexModulesTree, TPrefix extends string = never>
= UnionToIntersection<VuexCommitByModule<TModules, TPrefix>[keyof TModules]>
;
type VuexStore<TDefinition extends VuexStoreDefinition<any, any>>
= {
commit: VuexModuleCommit<TDefinition> & ((mutation: VuexMutations<TDefinition>, options?: VuexCommitOptions) => void)
}
;
type VuexMutation<TName extends string, TPayload = never>
= { type: TName }
& ([TPayload] extends [never] ? { } : { payload: TPayload })
type VuexOwnMutations<TModule extends VuexModule<any, any, any>, TPrefix extends string = never>
= {
[TMutation in keyof TModule["mutations"]]: VuexMutation<
AddPrefix<string & TMutation, TPrefix>,
VuexMutationHandlerPayload<TModule["mutations"][TMutation]>
>
}[keyof TModule["mutations"]]
type VuexMutations<TModule extends VuexModule<any, any, any>, TPrefix extends string = never>
= VuexOwnMutations<TModule, TPrefix>
| {
[TSubModule in keyof TModule["modules"]]:
VuexMutations<
TModule["modules"][TSubModule],
AddPrefix<TModule["modules"][TSubModule] extends NamespacedVuexModule<any, any> ? (string & TSubModule) : never, TPrefix>
>
}[keyof TModule["modules"]];
type VuexMutationPayload<
TModule extends VuexModule<any, any, any>,
TMutation extends string
>
= VuexMutations<TModule> extends VuexMutation<TMutation, infer TPayload>
? TPayload
: never
;
// example store definition
type FooState = { list: string[] }
type BarState = { result: string }
type BazState = { current: number }
enum FooMutations {
Added = "added",
Removed = "removed",
}
enum BarMutations {
Fizz = "fizz",
Buzz = "buzz",
}
enum BazMutations {
Inc = "inc",
Dec = "dec",
}
type FooMutationTree = {
[FooMutations.Added]: VuexMutationHandler<FooState, string>
[FooMutations.Removed]: VuexMutationHandler<FooState, number>
}
type BarMutationTree = {
[BarMutations.Fizz]: VuexMutationHandler<BarState, number>;
[BarMutations.Buzz]: VuexMutationHandler<BarState>;
}
type BazMutationTree = {
[BazMutations.Inc]: VuexMutationHandler<BazState, number>;
[BazMutations.Dec]: VuexMutationHandler<BazState, number>;
}
type FooModule = NamespacedVuexModule<FooMutationTree, { sub: BazModule }>;
type BarModule = GlobalVuexModule<BarMutationTree, {}>;
type BazModule = NamespacedVuexModule<BazMutationTree, {}>;
type MyStore = {
modules: {
foo: FooModule,
bar: BarModule,
anotherFoo: FooModule,
},
mutations: {}
}
// test
type Test = VuexStore<MyStore>;
type Debug = VuexMutationPayload<BazModule, BazMutations.Inc>;
declare function createStore<TDefinition extends VuexStoreDefinition<any, any>>(definition: TDefinition): VuexStore<TDefinition>;
let store = createStore<MyStore>({} as any)
store.commit("anotherFoo/sub/dec", 0); // should autocomplete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment