2 votes

Comment obtenir l'intellisense des mapGetters, mapActions Vuex et typescript sans la syntaxe de style de classe ou de décorateurs

J'utilise Vue.js et Vuex depuis un certain temps, mais toujours avec javascript.

Je cherche à utiliser Vue avec Typescript, plus précisément avec nuxt.js, mais sans utiliser de décorateurs ni de style-class-component, en continuant simplement avec la syntaxe normale de Vue

Voici le code que j'ai dans mon magasin Vuex

/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: 'premier todo',
      done: true
    },
    {
      id: 2,
      text: 'deuxième 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

/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

/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

Ceci est le code que j'ai dans mon composant,

import Vue from 'vue'
import { mapGetters, mapActions } from 'vuex'

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

Tout fonctionne parfaitement, sauf l'autocomplétion / l'intelligence de l'action des getters ou des actions provenant de Vuex

Quelqu'un peut m'aider?

Merci pour cela o/

1voto

Andrei Gheorghiu Points 3898

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 @Actions, 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...)
 */

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X