Vuex + TypeScript でモジュール分割する
Vuex + TypeScript でモジュール分割する Vue CLI を使って、TypeScript ベースの Vue.js プロジェクトを作成しました。状態管理ライブラリである Vuex も導入しましたが、 モジュール分割する際の型定義の仕方 で少し迷ったので、自分が見付けた対応方法を紹介します。
環境情報
Vue CLI (@vue/cli) : v4.5.13
TypeScript (typescript) : v4.1.6
Vue (vue) : v2.6.14
Vuex (vuex) : v3.6.2
Vuex のモジュール分割
Vuex で管理する内容が増えていくと管理が煩雑になるので、機能単位などでファイルを分割したくなります。Vuex には Modules という仕組みがあり、Namespace (名前空間) の概念と組み合わせると、見通しが良くなります。
参考 : Modules | Vuex
通常の JavaScript での実装は、上述の公式ページのとおり、modules: 配下に連想配列をネストしていけば良いので簡単です。 TypeScript で実装する際も、基本的には同じ考え方なのですが、せっかくなら型定義の恩恵を受けたいです。
このように実装しました
Vuex の Module という単位で型定義してあげると綺麗に扱えたので、その手順を紹介します。
./src/store/index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import { childStoreModule } from './child-store';
Vue.use(Vuex);
/** 親 State */
const rootState = {
/** サンプルとして適当な内容を用意しておきます */
count: 0
};
/** 親 State の型 : 子 Store の型定義に使用する */
export type RootState = typeof rootState;
/** 親 Store */
export default new Vuex.Store({
state: rootState,
getters: {},
mutations: {},
actions: {},
modules: {
child: childStoreModule
}
});
ルートとなる Store はこのように実装します。state: プロパティに設定する連想配列を外出しし、type (Type Aliases) を export しておきます。 modules: プロパティで child という名前空間名を指定しています。その内容として childStoreModule なるものを import しています。次はこの内容を見ていきます。
./src/store/child-store.ts
import { Module } from 'vuex';
import { RootState } from './index';
/** 子 State */
const childState = {
/** サンプルとして適当な内容を用意しておきます */
count: 0
};
/** 子 State の型 */
type ChildState = typeof childState;
/** 子 Store Module */
export const childStoreModule: Module<ChildState, RootState> = {
namespaced: true,
state: childState,
getters: {},
mutations: {},
actions: {},
modules: {}
});
こちらが、子 Module としたいファイルです。注目すべきは、連想配列に対し Module<ChildState, RootState> と型定義しているところです。これにより、getters や mutations というプロパティ名を Typo していたりするとうまく検知してくれるようになります。ジェネリクスとして自身の State とルート State を指定するため、先程 export した RootState という Type Aliases を import して利用しています。ChildState の用意の仕方は RootState と同じです。 子 Module は namespaced: true を指定して、名前空間を区切っています。子 Module も modules: プロパティを持っていますので、やろうと思えば子モジュールの下に孫モジュール以下を作っていくこともできます。その際、ジェネリクスで指定する State は Module<GrandchildState, RootState> というように、第2引数のジェネリクスは常に RootState を指定するようです (ChildState を書いてしまっても特段エラーにはなりませんでした)。
コンポーネントからの呼び出し方
上述のコードでは getters や mutations 内の実装を省略していますが、おおよそ次のように呼び出せるイメージが掴めるのではないでしょうか。
// RootState の count プロパティを取得する
const rootCount = this.$data.getters['count'];
// ChildState の count プロパティを取得する
const childCount = this.$data.getters['child/count'];
child/ という名前空間で区切っているので、count という名前が重複していたとしても、混同されることはありません。
他にもやり方はあるようですが、多分これが簡単です
この他にも、クラスベースでデコレータを使って実装するようなライブラリがあったり、Typed Vuex というラッパーライブラリを使う方法などもありましたが、今回紹介した方法は追加インストールするパッケージがなく、型定義のための実装量も最小限で済む割に、効率的なコーディングやコードリーディング時に必要な型定義は必要十分にできているのではないでしょうか。 TypeScript に詳しくなく、型管理のためのパッケージや実装コストが気になる場合でも、any で誤魔化すことは TypeScript を使うメリットが大幅に失われてしまうので、出来るだけ避けたいところです。今回のように簡単な実装で適度に型定義出来ているだけでも違うと思います。