DEV Community

shubhadip
shubhadip

Posted on

Vue 3, Vuex 4 Modules, Typescript

In this article we will going to see how can we use typescript along with Vue 3 and Vuex 4 and Vuex Modules.

Note: Vuex 4 is still in beta version. Also along with removal of global this from vue, this.$store is also removed from vuex 4 and its upto developer to write typings for store.You can read more about from vuex-release-note

We will be creating modules like root and counter wherein counter will be module and root as RootState.Lets create a module folder inside store folder and an index.ts which will export our store.
Store Structure

we will also have interfaces.ts file which will have all interfaces for our store.

import { ActionContext } from "vuex";
import { MutationTypes as CounterMTypes } from "./modules/counter/mutation-types";
import { ActionTypes as CounterATypes } from "./modules/counter/action-types";

export interface IRootState {
  root: boolean;
  version: string;
}

export interface CounterStateTypes {
  counter?: number;
  rootDispatch?: boolean
}

export interface CounterGettersTypes {
  doubledCounter(state: CounterStateTypes): number;
  counterValue(state: CounterStateTypes): number;
}

export type CounterMutationsTypes<S = CounterStateTypes> = {
  [CounterMTypes.SET_COUNTER](state: S, payload: number): void;
  [CounterMTypes.RESET_COUNTER](state: S): void;
};

export type AugmentedActionContext = {
  commit<K extends keyof CounterMutationsTypes>(
    key: K,
    payload: Parameters<CounterMutationsTypes[K]>[1]
  ): ReturnType<CounterMutationsTypes[K]>;
} & Omit<ActionContext<CounterStateTypes, IRootState>, "commit">;

export interface CounterActionsTypes {
  [CounterATypes.GET_COUNTER](
    { commit }: AugmentedActionContext,
    payload: number
  ): void;
}
Enter fullscreen mode Exit fullscreen mode

so CounterActionsTypes acts as interface for actions in counter module, CounterMutationsTypes acts as interface for mutations in counter module and CounterGettersTypes acts as interface for getters.You can read more about utility like Omit etc from typescript-utility

Lets start by creating counter module.

// store/modules/counter/state.ts
import { CounterStateTypes } from "./../../interfaces";

export const state: CounterStateTypes = {
  counter: 0,
};

// store/modules/counter/action-types.ts
export enum ActionTypes {
  GET_COUNTER = "GET_COUNTER"
}

// store/modules/counter/mutation-types.ts
export enum MutationTypes {
  SET_COUNTER = "SET_COUNTER",
  RESET_COUNTER = "RESET_COUNTER"
}

// store/modules/counter/getters.ts

import { GetterTree } from "vuex";
import {
  CounterGettersTypes,
  CounterStateTypes,
  IRootState
} from "./../../interfaces";

export const getters: GetterTree<CounterStateTypes, IRootState> &
  CounterGettersTypes = {
  counterValue: (state: CounterStateTypes) => {
    return state.counter || 0;
  },
  doubledCounter: (state: CounterStateTypes) => {
    return state.counter || 0 * 2;
  }
};

// store/modules/counter/mutations.ts
import { MutationTree } from "vuex";
import { MutationTypes } from "./mutation-types";
import { CounterMutationsTypes, CounterStateTypes } from "./../../interfaces";

export const mutations: MutationTree<CounterStateTypes> &
  CounterMutationsTypes = {
  [MutationTypes.SET_COUNTER](state: CounterStateTypes, payload: number) {
    state.counter = payload;
  },
  [MutationTypes.RESET_COUNTER](state: CounterStateTypes) {
    state.counter = 0;
  }
};

// store/modules/counter/actions.ts
import { ActionTree } from "vuex";
import { ActionTypes } from "./action-types";
import { MutationTypes } from "./mutation-types";
import {
  CounterActionsTypes,
  CounterStateTypes,
  IRootState
} from "@/store/interfaces";

export const actions: ActionTree<CounterStateTypes, IRootState> &
  CounterActionsTypes = {
  [ActionTypes.GET_COUNTER]({ commit }, payload: number) {
    commit(MutationTypes.SET_COUNTER, payload);
  }
};

// store/modules/counter/index.ts
import { Module } from "vuex";
import { CounterStateTypes, IRootState } from "@/store/interfaces";
import { getters } from "./getters";
import { actions } from "./actions";
import { mutations } from "./mutations";
import { state } from "./state";

// Module
const counter: Module<CounterStateTypes, IRootState> = {
  state,
  getters,
  mutations,
  actions
};

export default counter;
Enter fullscreen mode Exit fullscreen mode

Now that we have created module counter we have to add types, lets create types.ts in counter module

// store/modules/counter/types.ts
import {
  CounterStateTypes,
  CounterMutationsTypes,
  CounterGettersTypes,
  CounterActionsTypes
} from "@/store/interfaces";
import { Store as VuexStore, CommitOptions, DispatchOptions } from "vuex";

export type CounterStoreModuleTypes<S = CounterStateTypes> = Omit<
  VuexStore<S>,
  "commit" | "getters" | "dispatch"
> & {
  commit<
    K extends keyof CounterMutationsTypes,
    P extends Parameters<CounterMutationsTypes[K]>[1]
  >(
    key: K,
    payload?: P,
    options?: CommitOptions
  ): ReturnType<CounterMutationsTypes[K]>;
} & {
  getters: {
    [K in keyof CounterGettersTypes]: ReturnType<CounterGettersTypes[K]>;
  };
} & {
  dispatch<K extends keyof CounterActionsTypes>(
    key: K,
    payload?: Parameters<CounterActionsTypes[K]>[1],
    options?: DispatchOptions
  ): ReturnType<CounterActionsTypes[K]>;
};
Enter fullscreen mode Exit fullscreen mode

So this creates our counter module along with its types.Lets now focus on root, we will be creating a root folder inside modules which will be used as root for vuex store.
store structure

This is how our folder structure will look like after this.
only extra thing that we have to do in root module is to add modules to it rest all is similar to counter module

// store/modules/root/index.ts

import { Module, ModuleTree } from "vuex";
import { IRootState } from "@/store/interfaces";
import { getters } from "./getters";
import { actions } from "./actions";
import { mutations } from "./mutations";
import { state } from "./state";
import counterModule from "../counter";

// Modules
const modules: ModuleTree<IRootState> = {
  counterModule,
};

const root: Module<IRootState, IRootState> = {
  state,
  getters,
  mutations,
  actions,
  modules
};

export default root;
Enter fullscreen mode Exit fullscreen mode

you can see we have added modules to root and we can pass this to createStore.

so with this done now we can set up index.ts in our store folder

import { createStore } from "vuex";
import { IRootState } from "@/store/interfaces";
import { CounterStoreModuleTypes } from "./modules/counter/types";
import { RootStoreModuleTypes } from "./modules/root/types";

import root from "./modules/root";

export const store = createStore<IRootState>(root);

type StoreModules = {
  counter: CounterStoreModuleTypes;
  root: RootStoreModuleTypes;
};

export type Store = CounterStoreModuleTypes<Pick<StoreModules, "counter">> &
  Counter1StoreModuleTypes<Pick<StoreModules, "counter1">> &
  RootStoreModuleTypes<Pick<StoreModules, "root">>;

Enter fullscreen mode Exit fullscreen mode

createStore<IRootState>(root); with this we are marking root module as rootState.Store will act as type for our whole store.

I have also created an action-types.ts and mutation-types.ts inside store folder so that we can have all actions and mutation at one place.

// store/action-types.ts
import { ActionTypes as counterTypes } from "./modules/counter/action-types";
import { ActionTypes as rootATypes } from "./modules/root/action-types";

export const AllActionTypes = { ...counterTypes, ...rootATypes };

// store/mutation-types.ts
import { MutationTypes as counterTypes } from "./modules/counter/mutation-types";
import { MutationTypes as rootMTypes } from "./modules/root/mutation-types";

export const AllMutationTypes = {...counterTypes,...rootMTypes };

Enter fullscreen mode Exit fullscreen mode

Note: since i am spreading types, in case of same action/mutation types might override. we can prevent this by using namespace or completely avoiding this.

This completes our store,lets see how we can use our store in components.
we will be creating a useStore utility in src/use folder.

import { Store } from "@/store";

import { useStore as VuexStore } from "vuex";
/**
 * Returns Whole Store Object
 */
export function useStore(): Store {
  return VuexStore() as Store;
}
Enter fullscreen mode Exit fullscreen mode

Now we can import useStore directly in our views and components.

type support

type support

In this way we can have type support for store as well as we added modules in store along with types.

Note : I have skipped few steps of creating store, all code regarding this article is present at Repository.
Articles which i went through before being able to get modules working vuex-typescript

Top comments (5)

Collapse
 
davepile profile image
davepile

This is a great article and repo. Thank you very much. Unfortunately it has convinced me to give up on TS with Vuex. I just cant see the value, but thank you for this. It was very informative

Collapse
 
rafarel profile image
Rafarel

I think it should be more verbose 🤣It's a great job you've done here but isn't that overkill? That amount of code for two counter is insane, no?

Collapse
 
rafarel profile image
Rafarel

I did not gave up the Vuex and typescript, right now I'm trying to get my modules namespaced. Any idea, on how to access namespaced modules ?

Collapse
 
shubhadip profile image
shubhadip • Edited

actually, it was a bit difficult to access namespaced modules, you might need to use github.com/greenpress/vuex-composi... library until vue 3 gives support for this: github.com/vuejs/vuex/issues/1725

Collapse
 
samcharles93 profile image
Sam

This is exactly the article I've been waiting for... I've used only parts of these in my store, but seeing how you've implemented this is fantastic. Thank you. 🙂