Search code examples
typescriptvue.jsvuexnuxt.js

How to get intellisense of mapGetters, mapActions Vuex and typescript without class-style or decorators syntax


I use Vue.js and Vuex for some time, but always with javascript.

I'm trying to use Vue with Typescript, nuxt.js to be more specifically, but without using decorators or style-class-component, only continue with the normal Vue syntax

This is the code I have in my Vuex store

/store/todos/types.ts

export interface Todo {
  id: number
  text: string
  done: boolean
}

export interface TodoState {
  list: Todo[]
}

/store/todos/state.ts

import { TodoState } from './types'

export default (): TodoState => ({
  list: [
    {
      id: 1,
      text: 'first todo',
      done: true
    },
    {
      id: 2,
      text: 'second todo',
      done: false
    }
  ]
})

/store/todos/mutations.ts

import { MutationTree } from 'vuex'
import { TodoState, Todo } from './types'

export default {
  remove(state, { id }: Todo) {
    const index = state.list.findIndex((x) => x.id === id)
    state.list.splice(index, 1)
  }
} as MutationTree<TodoState>

/store/todos/actions.ts

import { ActionTree } from 'vuex'
import { RootState } from '../types'
import { TodoState, Todo } from './types'

export default {
  delete({ commit }, { id }: Todo): void {
    commit('remove', id)
  }
} as ActionTree<TodoState, RootState>

/store/todos/getters.ts

import { GetterTree } from 'vuex'
import { RootState } from '../types'
import { TodoState, Todo } from './types'

export default {
  list(state): Todo[] {
    return state.list
  }
} as GetterTree<TodoState, RootState>

This is code that I have my component,

<template>
  <div>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.text }}
        <button @click="destroy(todo)">delete</button>
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { mapGetters, mapActions } from 'vuex'

export default Vue.extend({
  computed: {
    ...mapGetters({
      todos: 'todos/list'
    })
  },
  methods: {
    ...mapActions({
      destroy: 'todos/delete'
    })
  }
})
</script>

Everything works perfectly, except the auto complete / intellisense of the getters or actions that came from Vuex

Someone can help me?

Thanks for this o/


Solution

  • Vuex, in current form, doesn't work well with Typescript. That's probably going to change in Vue 3.

    Just as you, I also don't want to use @Component decorators, especially because they have been deprecated. However, when it comes to using the default Vue typescript component style:

    <script lang="ts">
      import Vue from 'vue';
      export default Vue.extend({...})
    </script>
    

    ... after testing multiple solutions I found the easiest to use is actually a plugin which does use decorators: vuex-module-decorators

    Vuex module:

    I typically leave the parent state clean (empty) and use namespaced modules. I do it mostly because more than once I decided at the end of the project it would be cleaner to have more than one module, and it's more of a hassle to move it from parent to module than to simply create an additional module.

    The store looks like this:

    import Vue from 'vue';
    import Vuex from 'vuex';
    import { getModule } from 'vuex-module-decorators';
    import Whatever from '@/store/whatever';
    
    Vue.use(Vuex);
    
    const store = new Vuex.Store({
      modules: {
        whatever: Whatever
      }
    });
    
    getModule(Whatever, store); // this is important for typescript to work properly
    
    export type State = typeof store.state;
    export default store;
    

    Here are a few examples of mapState, mapGetters or get/set computed that work directly with the store:

    computed: {
      ...mapGetters({
        foo: 'whatever/foo',
        bar: 'whatever/bar'
      }),
      ...mapState({
        prop1: (state: State): prop1Type[] => state.whatever.prop1,
        prop2: (state: State): number | null => state.whatever.prop2
      }),
      // if i want get/set, for a v-model in template
      baz: {
        get: function(): number {
          return this.$store.state.whatever.baz;
        },
        set: function(value: number) {
          if (value !== this.baz) { // read * Note 1
            this.$store.dispatch('whatever/setBaz', value);
            // setBaz can be an `@Action` or a `@MutationAction`
          }
        }
      }
    }
    

    baz can now be used in a v-model. Note mapGetters need to be actual module store getters:

    import { $http, $store } from '@/main'; // read * Note 2
    import { Action, Module, Mutation, MutationAction, VuexModule } from 'vuex-module-decorators';
    
    @Module({ namespaced: true, store: $store, name: 'whatever' })
    export default class Whatever extends VuexModule {
    
      get foo() {
        return // something. `this` refers to class Whatever and it's typed
      }
      baz = 0;
      prop1 = [] as prop1Type[];       // here you cast the type you'll get throughout the app
      prop2 = null as null | number;   // I tend not to mix types, but there are valid cases 
                                       // where `0` is to be treated differently than `null`, so...
      @MutationAction({ mutate: ['baz'] })
      async setBaz(baz: number) {
        return { baz }
      }
    }
    

    Now, you won't have any trouble using @Action or @Mutation decorators and you can stop there, you won't have any typescript problems. But, because I like them, I find myself using @MutationActions a lot, even though, to be fair, they're a hybrid. A hack, if you want.
    Inside a @MutationAction, this is not the module class. It's an ActionContext (basically what the first param in a normal js vuex action would be):

    interface ActionContext<S, R> {
      dispatch: Dispatch;
      commit: Commit;
      state: S;
      getters: any;
      rootState: R;
      rootGetters: any;
    }
    

    And that's not even the problem. The problem is Typescript thinks this is the module class inside a @MutationAction. And here's when you need to start casting or use typeguards. As a general rule, I try to keep casting to a minimum and I never use any. Typeguards can go a long way.
    The golden rule is: If I need to cast as any or as unknown as SomeType, it's a clear sign I should split the @MutationAction into an @Action and a @Mutation. But in vast majority of cases, a typeguard is enough. Example:

    import { get } from 'lodash';
    ...
    @Module({ namespaced: true, store: $store, name: 'whatever' })
    export default class Whatever extends VuexModule {
      @MutationAction({ mutate: ['someStateProp'] })
      async someMutationAction() {
        const boo = get(this, 'getters.boo'); // or `get(this, 'state.boo')`, etc...
        if (boo instaceof Boo) {
          // boo is properly typed inside a typeguard
          // depending on what boo is, you could use other typeguards:
          // `is`, `in`, `typeof`  
        }
    }
    

    If you only need the values of state or getters: this.state?.prop1 || [] or this.getters?.foo also work.

    In all fairness, @MutationAction requires some form of type hacking, since you need to declare the types: they are not inferred properly. So, if you want to be 100% correct, limit their usage to cases where you're simply setting the value of a state property and you want to save having to write both the action and the mutation:

    @MutationAction({ mutate: ['items'] })
    async setItems(items: Item[]) {
      return { items }
    }
    

    Which replaces:

    @Action
    setItems(items: Item[]) {
      this.context.commit('setItems', items);
      // btw, if you want to call other @Action from here or any @MutationAction
      // they work as `this.someAction();` or `this.someMutationAction()`;
    }
    
    @Mutation
    setItems(items: Item[]) {
      this.items = items;
    }
    

    @MutationActions are registered as @Actions, they take a { mutate: [/* full list of props to be mutated*/]} and return an object having all the declared state props which are declared in the array of props to be mutated.

    That's about it.


    * Note 1: I had to use that check when I used two different inputs (a normal one and a slider input) on the same get/set v-model. Without that check, each of them would trigger a set when updated, resulting in a stack-overflow error. You normally don't need that check when you only have 1 input.

    * Note 2: here's how my main.ts typically looks like

    import ...
    Vue.use(...);
    Vue.config...
    
    const Instance = new Vue({
      ...
    }).$mount(App);
    
    // anything I might want to import in components, store modules or tests:
    export { $store, $t, $http, $bus } = Instance; 
    /* I'd say I use these imports more for correct typing than for anything else 
     (since they're already available on `this` in any component). But they're 
     quite useful outside of components (in services, helpers, store, translation 
     files, tests, etc...)
     */