'How to use TypeScript + Composition API + Vuex (namespaced)?

I'm currently trying to create a Vue 3 Application that utilizes the Composition API with Vuex 4 (namespaced) written in TypeScript.

There are tons of different approaches but none of them look very useable as of now. I don't want to use any class style syntax or unnecessary external wrappers for this.

Here's a minimal example to show you my issues:

// package.json
"vue": "^3.0.0",
"vuex": "^4.0.2"
// store.ts
import {Store as VuexStore, createStore, Module, ActionTree} from "vuex";

export enum ModuleActionTypes {
    Test = 'TEST'
}

interface ModuleActions {
    [ModuleActionTypes.Test]():void
}

const actions: ActionTree<any, any> & ModuleActions = {
    [ModuleActionTypes.Test]() : void {
        console.log("test action dispatched")
    }
}

type ModuleStore = Omit<VuexStore<any>, 'dispatch'>
    & {
        dispatch<K extends keyof ModuleActions>(
            key: K
        ): ReturnType<ModuleActions[K]>
    }

const module: Module<any, any> = {
    namespaced: true,
    actions: actions
}

type Store = ModuleStore

const store = createStore<Store>({
    modules: {
        module
    }
})

export function useStore(): Store {
    return store
}

The interesting or tricky part is the invocation from an app perspective. I've tried several approaches with different outcomes.

Approach 1 (failed): Built-in createNamespacedHelpers() with mapActions or useActions from the vuex package do not work since it requires this.$store to be set which is not the case with Vue 3 (Composition API) and Vuex 4.

GitHub request for use*

Approach 2 (failed): Utilizing vuex-composition-helpers requires the dependency to the @vue/composition-api which causes a dependency nightmare when including into the Vue 3 dependency graph.

Approach 3 (failed): Moving closer to code: utilizing the typed ModuleActionTypes to dispatch an action results in a [vuex] unknown action type: TEST error since useStore() returns the general store instead of the submodule. Removing the namespace from the module would obviously make it work.

...
// App.vue
<script lang="ts">
  import {defineComponent} from "vue";
  import {ModuleActionTypes, useStore} from "./store";

  export default defineComponent({
    name: 'App',
    setup() {
      const store = useStore()
      store.dispatch(ModuleActionTypes.Test)
    }
  })
</script>
...

Approach 4 (working): "un-type" the module's store and use a plain string to dispatch the action.

// store.ts
...
type ModuleStore = VuexStore<any>
...

// App.vue
...
    setup() {
      const store = useStore()
      store.dispatch(`module/${ModuleActionTypes.Test}`)
    }
...

Additionally, internal and external action types could be used @see 47646176 to avoid the string concatenation within the Vue component.

Are there any ideas or suggestions where the store of the submodule can still be typed and namespaces are in place?



Solution 1:[1]

I came across this problem today and figured out a way how this could be tackled.

The problem is mainly that when using the useStore() method with Action-, Mutation-, or GetterTypes does not automatically add a namespace. I opted for a solution using generics and wrapping the store in a function which adds the namespace for the getters, dispatch, commit, and state functionality.

// store/index.ts
export const rootStore = createStore({
  modules: {
    app: appModule,
  }
});

export const rootStoreKey: InjectionKey<Store<RootState>> = Symbol()

function rootStoreToNamespacedStore<ActionTypes, Actions extends { [key: string]: any }, MutationsTypes, Mutations extends { [key: string]: any }, GetterTypes, Getters extends { [key: string]: any }, StateType>(namespace: string, store: Store<any>) {
  return {
    getters<K extends keyof Getters>(getterType: GetterTypes): ReturnType<Getters[K]> {
      return store.getters[`${namespace}/${getterType}`];
    },
    dispatch<K extends keyof Actions>(payloadWithType: ActionTypes, payload: Parameters<Actions[K]>[1], options?: DispatchOptions): ReturnType<Actions[K]> {
      return store.dispatch(`${namespace}/${payloadWithType}`, payload, options) as ReturnType<Actions[K]>;
    },
    commit<K extends keyof Mutations>(payloadWithType: MutationsTypes, payload: Parameters<Mutations[K]>[1], options?: CommitOptions): void {
      return store.commit(`${namespace}/${payloadWithType}`, payload, options)
    },
    state: store.state[namespace] as StateType
  };
}

export function useAppStore() {
  const store = useStore(rootStoreKey);
  return rootStoreToNamespacedStore<AppActionTypes, AppActions, AppMutationTypes, AppMutations, AppGetterTypes, AppGetters, IAppState>('app', store);
}

This will allow you to use it in the same way you are used to.

// some/component.vue
...
setup(props, { emit }) {
    const appStore= useAppStore();
    const count = computed(() => appStore.getters(AppGetterTypes.GET_COUNT));
    
    // or access via the state
    // const count = computed(() => appStore.state.count);

    // dispatching using your defined types
    appStore.dispatch(AppActionTypes.SET_COUNT, count+1);

    return {
      count,
    };
  },
...

If you would like a more complete example, have a look how I implemented it on my Github modules example

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1