Vuex, sous sa forme actuelle, ne fonctionne pas bien avec Typescript. Cela va probablement changer avec Vue 3.
Tout comme vous, je ne veux pas utiliser les décorateurs @Component
, en particulier parce qu'ils ont été dépréciés. Cependant, en ce qui concerne l'utilisation du style par défaut des composants Vue TypeScript :
import Vue from 'vue';
export default Vue.extend({...})
... après avoir testé plusieurs solutions, j'ai trouvé que la plus facile à utiliser est en fait un plugin qui utilise des décorateurs : vuex-module-decorators
Module Vuex :
Je laisse généralement l'état parent propre (vide) et j'utilise des modules avec des espaces de noms. Je le fais surtout parce qu'à plusieurs reprises j'ai décidé à la fin du projet qu'il serait plus propre d'avoir plus d'un module, et c'est plus compliqué de le déplacer du parent au module que de simplement créer un module supplémentaire.
Le magasin ressemble à ceci :
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); // c'est important pour que Typescript fonctionne correctement
export type State = typeof store.state;
export default store;
Voici quelques exemples de mapState
, mapGetters
ou computed avec get/set qui fonctionnent directement avec le 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
}),
// si je veux get/set, pour un v-model dans le template
baz: {
get: function(): number {
return this.$store.state.whatever.baz;
},
set: function(value: number) {
if (value !== this.baz) { // lire * Note 1
this.$store.dispatch('whatever/setBaz', value);
// setBaz peut être une `@Action` ou une `@MutationAction`
}
}
}
}
baz
peut maintenant être utilisé dans un v-model
. Notez que mapGetters
doit être réellement des getters de modules du store :
import { $http, $store } from '@/main'; // lire * 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 // quelque chose. `this` se réfère à la classe Whatever et est typé
}
baz = 0;
prop1 = [] as prop1Type[]; // ici vous castez le type que vous obtiendrez dans toute l'application
prop2 = null as null | number; // J'ai tendance à ne pas mélanger les types, mais il y a des cas valides
// où `0` doit être traité différemment de `null`, donc...
@MutationAction({ mutate: ['baz'] })
async setBaz(baz: number) {
return { baz }
}
}
Maintenant, vous n'aurez aucun problème à utiliser les décorateurs @Action
ou @Mutation
et vous pouvez vous arrêter là, vous n'aurez aucun problème TypeScript. Mais, parce que je les aime, je me retrouve à utiliser beaucoup les @MutationAction
, même si, pour être honnête, ce sont des hybrides. Un hack, si vous voulez.
À l'intérieur d'une @MutationAction
, this
n'est pas la classe de module. C'est un ActionContext (essentiellement ce que serait le premier paramètre dans une action vuex js normale) :
interface ActionContext {
dispatch: Dispatch;
commit: Commit;
state: S;
getters: any;
rootState: R;
rootGetters: any;
}
Et ce n'est même pas le problème. Le problème est que Typescript pense que this
est la classe de module à l'intérieur d'un @MutationAction
. C'est là que vous devez commencer à caster ou utiliser des typeguards. En règle générale, j'essaie de limiter les castings au strict minimum et je n'utilise jamais any
. Les typeguards peuvent aller loin.
La règle d'or est : Si j'ai besoin de caster as any
ou as unknown as SomeType
, c'est un signe clair que je devrais diviser le @MutationAction
en un @Action
et une @Mutation
. Mais dans la grande majorité des cas, une typeguard suffit. Exemple :
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'); // ou `get(this, 'state.boo')`, etc...
if (boo instaceof Boo) {
// boo est correctement typé à l'intérieur d'une typeguard
// en fonction de ce qu'est boo, vous pourriez utiliser d'autres typeguards :
// `is`, `in`, `typeof`
}
}
Si vous avez seulement besoin des valeurs de state
ou getters
: this.state?.prop1 || []
ou this.getters?.foo
fonctionnent aussi.
En toute honnêteté, @MutationAction
nécessite une forme de piratage de type, car vous devez déclarer les types : ils ne sont pas inférés correctement. Donc, si vous voulez être à 100% correct, limitez leur utilisation aux cas où vous définissez simplement la valeur d'une propriété d'état et où vous voulez éviter d'écrire à la fois l'action et la mutation :
@MutationAction({ mutate: ['items'] })
async setItems(items: Item[]) {
return { items }
}
Cela remplace :
@Action
setItems(items: Item[]) {
this.context.commit('setItems', items);
// au fait, si vous voulez appeler d'autres @Action depuis ici ou n'importe quelle @MutationAction
// elles fonctionnent comme `this.someAction();` ou `this.someMutationAction()`;
}
@Mutation
setItems(items: Item[]) {
this.items = items;
}
Les @MutationAction
sont enregistrés en tant que @Action
s, ils prennent un { mutate: [/* liste complète des props à muter*/]}
et retournent un objet ayant toutes les props d'état déclarées qui sont déclarées dans le tableau de props à muter.
C'est à peu près tout.
* Note 1 : J'ai dû utiliser cette vérification lorsque j'ai utilisé deux entrées différentes (une normale et une glissière) sur le même get/set
v-model
. Sans cette vérification, chacune d'entre elles déclencherait un set
lorsqu'elles seraient mises à jour, entraînant une erreur de débordement de pile. Vous n'avez normalement pas besoin de cette vérification lorsque vous avez une seule entrée.
* Note 2 : voici à quoi ressemble typiquement mon fichier main.ts
import ...
Vue.use(...);
Vue.config...
const Instance = new Vue({
...
}).$mount(App);
// tout ce que je pourrais vouloir importer dans les composants, les modules de magasin ou les tests :
export { $store, $t, $http, $bus } = Instance;
/* Je dirais que j'utilise ces imports plus pour le typage correct que pour autre chose
(puisqu'ils sont déjà disponibles sur `this` dans n'importe quel composant). Mais ils
sont très utiles en dehors des composants (dans les services, les helpers, le magasin, les
fichiers de traduction, les tests, etc...)
*/