DEV Community

Dave Correll
Dave Correll

Posted on

Strongly typed VueX store in four lines of TypeScript

The issue

I was lucky enough to get to work on a Vue/TypeScript project at work recently, and a common issue developers complained about was the lack of typing in the store.

Typically our files looked like this, and threw no type errors:

import Vue from 'vue';

export default Vue.extend({
    name: 'whatever',
    computed: {
        getVariable(): string | undefined {
            return this.$store.state.LITERALLY.ANY.THING;
        },
    },
});

This works, but it would be nice to utilize all of the state interfaces that we defined for use with VueX.

The existing definitions

Looking at the Vuex definition file, $store has a type of Store<any>.

vuex/types/vue.d.ts

declare module "vue/types/vue" {
  interface Vue {
    $store: Store<any>;
  }
}

The plan here is to change the type Store<any> into Store<RootStore>.

There is one issue though, just amending this definition will not be enough. We use Vue.extend() to create our components, and the type of Vue instance returned from that method does not contain our custom $store definition.

Looking at the original Vue definition, all of the .extend methods include the V type variable in the return type, which is Vue by default.

vue/types/vue.d.ts

export interface VueConstructor<V extends Vue = Vue> {
...
  extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;
...
}

The solution

If we pass a different type variable into VueConstructor, that should return the properly typed store instance.

Note: RootState is the interface we created to represent our root state. I don't believe this is very uncommon, but it's included for reference.

export interface RootState {
    loaded: Boolean
}

In this example, I'm using the name StrongVue to represent Vue being strongly typed. Super clever.

I created this file in it's own folder, and since it'll be used from now on instead of the standard Vue object, I put it somewhere easily accessible.

strong-vue/strong-vue.ts

import Vue, { VueConstructor } from 'vue'
import { Store } from 'vuex'
import { RootState } from '../store/types'

abstract class StrongVueClass extends Vue {
    public $store!: Store<RootState>;
}
const StrongVue = Vue as VueConstructor<StrongVueClass>;

export default StrongVue;

We create an abstract class that extends Vue, and redefines the $store type from Store<any>to Store<RootState>. The final step is to cast the imported standard Vue object to our new VueConstructor<StrongVueClass>, and export that.

So now, instead of importing Vue directly - we use our custom definition:

import StrongVue from '../strong-vue/strong-vue'

StrongVue.extend({
    computed: {
        getVariable(): boolean {
            return this.$store.state.LITERALLY.ANY.THING; //will throw type error
            return this.$store.state.loaded; //gives no type error
        },
    },
})

I haven't found anything online about this solution, and it's been pretty reliable so far, as long as you keep your RootState up to date.

Thanks.

Top comments (2)

Collapse
 
robingoupil profile image
Robin Goupil

Good tip, work well.
We adapted this trick to pass the store type through a generic like this, so that it can work in a multi store environment:

abstract class StrongVueClass<S extends Store<any>> extends Vue {
    public $store!: S;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
daroco profile image
Dave Correll

A few years late - but I'm glad this helped you!!!