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/
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 @MutationAction
s 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;
}
@MutationAction
s are registered as @Action
s, 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...)
*/