TypeScript
Vue.js
Vuex

vuex + typescriptをvuex-module-decoratorsで無敵になる


vuex + typescript をvuex-modules-decoratorで無敵になる


はじめに

vue.js, vuex, typescriptを用いた開発環境で、コンポーネント内からvuexのgetters, state, actions, mutationを用いようとしたとき、インテリセンスが効かない、型安全が保たれないといった問題に直面する。

これらを解決する策としてサードパーティー製のパッケージvuex-module-decoratorsを用いたのでここに記す。


Vuex + typescriptは発展途上

vuex + typescriptで開発を行うとgetters, mutationでせっかく型指定をしても型安全を保てない。

各mutationやgettersはコンポーネントで呼び出す際、文字列による指定を取るためインテリセンスが効かない

Image

↑インテリセンスが効いてない悲しみのgetters (最初の候補はなぜかcssのheightから出てる)


特に何も考えずvuexを用いた場合

vuexのstore stateにfugaをもっており、fugaに対してgetters, mutationを用意


counter.ts

 import Vue from "vue";

import Vuex from "vuex";
Vue.use(Vuex);
interface State {
counter: number;
}

const state: State = {
counter: 0
};

const getters = {
counter: (state: State) => state.fuga
};
const mutations: MutationTree<State> = {
COMMIT_COUNTER: (state, data: number) => (state.fuga = data),
}
export default new Vuex.Store<State>({
state,
getters,
mutations
});


これをコンポーネントで呼ぼうとすると、次のようになってしまう。

vuexをtypescriptで扱おうとすると型が守られない、インテリセンスが効かない状態になり、非常に使い辛い。


component.ts

 import Vue from "vue";

import Component from "vue-class-component"

@Component({
name: "contracts-expand"
})
export default class extends Vue {
// inputから受け取った値をvuexのstate fugaに格納
set fuga(inputNumber: number) {
this.$store.commit("COMMIT_COUNTER", inputNumber);
// ↑ここもcommitのfunctionを指定するのに文字列でインテリセンスが効かない
// さらに型は何でも渡せてしまう。(counterはnumber型だが、string型等を渡しても別にエラーにならないし書き換わる)
}
// vuexのstateに格納されているcounterを取得
get fuga() {
return this.$store.getters.counter // ← このcounterにはインテリセンスが効かない。
}
}



vuex-modules-decoratorを用いて無敵になる

vuexのmoduleをクラス化して、そのインスタンスをコンポーネントでそれを引っ張り出すことによってインテリセンスを効かせるサードパーティー製のライブラリを導入する。デコレータのおかげでgetters, mutation, actions, stateのネストが一階層さがるのもいい。


counter.ts

import { Mutation, MutationAction, Action, VuexModule, getModule, Module } from "vuex-module-decorators";

import store from "@/store/store"; // デコレータでstoreを指定するためimportする必要あり

// state's interface
export interface ICounterState {
incrementCounter: number; // 数字が増えてくカウンター
decrementCounter: number; // 数字が減ってくカウンター
}
@Module({ dynamic: true, store, name: "counter", namespaced: true })
class Counter extends VuexModule implements ICounterState {
// state
incrementCounter: number = 0;
decrementCounter: number = 1000;
// mutation
@Mutation
public SET_INCREMENT_COUNTER(num: number) {
this.incrementCounter = num;
}
@Mutation
public SET_DECREMENT_COUNTER(num: number) {
this.decrementCounter = num;
}
// actions
@Action({})
// カウンターに100加算するアクション
public increment100() {
// actions内で簡単にthisからmutationを呼び出せる。
this.SET_INCREMENT_COUNTER(this.incrementCounter + 100);
}
@Action({})
// カウンターに100減算するアクション
public decrement100() {
this.SET_DECREMENT_COUNTER(this.decrementCounter - 100);
}
// actions + mutation
// incrementCounter decrementCounter両方をリセットするアクションとミューテーション
@MutationAction({mutate: ["incrementCounter", "decrementCounter"]})
async resetCounter() {
return {
incrementCounter: 0,
decrementCounter: 1000
};
}
}

export const counterModule = getModule(Counter);


storeはvuexのstoreを示しており、このモジュールがどのstoreに属しているのかを@Moduleで明示的に示す必要がある。

またmoduleがclass化されたので、それぞれのメソッドの中でthisを使ってお互いを呼び出すことができる。

特に優れている点がactionsでのmutationの呼び出しである。

mutation SET_INCREMENT_COUNTERは、引数にnumber型の値を要求しており、コンポーネントやactionsで呼び出したときも、vuexでの型チェックがしっかり行われることがわかる。

従来のvuexのactions内でのmutationやactionsの呼び方では


mutation.ts

commit("SET_INCREMENT_COUNTER", "any") // ←なんでも受け付ける


このような書き方になり、型チェックができなかった。

それが

this.SET_INCREMENT_COUNTER(num) // ←numはnumber型でないとエラーを吐く


コンポーネントでの呼び出し

コンポーネントではgetModuleでオブジェクト化されたクラスをimportする。


component.ts

import { Component, Vue } from "vue-property-decorator";

import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
import { counterModule } from "@/store/modules/counter"; // モジュールクラスをインポート
@Component({
components: {
HelloWorld
}
})
export default class Home extends Vue {
get incrementCounter() {
return counterModule.incrementCounter; // インテリセンスが効く
}
get decrementCounter() {
return counterModule.decrementCounter; // インテリセンスが
}
increment() {
counterModule.SET_INCREMENT_COUNTER(counterModule.incrementCounter + 1); // 型チェックが効く
}
decrement() {
counterModule.SET_DECREMENT_COUNTER(counterModule.decrementCounter - 1); // 型チェック
}
increment100() {
counterModule.increment100(); // インテリセンスが効く
}
decrement100() {
counterModule.decrement100(); // インテリセンスが
}
resetCounter() {
counterModule.resetCounter(); // イ
}
}

コンポーネントではモジュールクラスをインポートすればインテリセンスが効きまくり無事無敵になる。こうして人類はactionsやgettersを呼ぶときに一字一句間違えず変数名を書くことや、手動で名前空間用の変数名格納ファイルを作ることから解放された。

Image

最高ですね。

image.png

SET_INCREMENT_COUNTERの引数にnumber型が要求されていることもわかる。


問題点


命名規則について

vuexのgetters mutations state actions、全てをモジュール単位で一つのクラスに格納するので名前に明確なルールを敷かないと大きなモジュールになった際、どれがどれだかわからなくなり困ることは明白。なので名前にルールを敷いて解決するのがベター。

個人的にmutationとgettersはスネークケースのSET,GET始まりの名前で、actionsはmethodだからキャメルケースで自由。stateもキャメルケースで自由。といった構成がわかりやすくていいじゃないかと思う。


MutationActionsについて

デコレータMutationActionsを用いればデコレータ内で宣言されているstateに対してcommitを一気に行うことができる。returnにJSON形式でmutateする値を指定することができる。上のコードでもカウンターをリセットするためのものとして用意してみた。

    @MutationAction({mutate: ["incrementCounter", "decrementCounter"]}) // incrementCounterとdecrementCounterを選択

async resetCounter() {
return { // return値はincrementCounterとdecrementCounterのJSON型で表記する
incrementCounter: 0,
decrementCounter: 1000
};
}

これはこれで便利だが、どうしてもstateを文字列指定してしまっているし、返り値のJSON形式の型チェックまで行われない。

そのため、incrementCounterdecrementCounterもnumber型と明記されているが、べつに何を入れてもエラーが出ない。

便利だがこの辺は割と残念なところだ。

MutationActionは便利だが極力使わないようにするほうがいい、というのが持論。


最後に

vuex + typescriptの開発は悩んでいる人が多く、型安全性とインテリセンスに困らされる人がまだ多い印象だ。

vuex-module-deccoratorsのおかげでmapActions, mapGettersから完全に手をひくことができたのでとても感謝している。

私が調べた当時qiitaの記事に、vuex-module-deccoratorsの紹介がなかったことは非常に残念なことで、もし悩んでる人がいたらその助けになれれば幸いだ。


参照 資料

参考

Writing Vuex modules in neat Typescript classes

vuex-module-decorators

リポジトリ

サンプルソースコード