⛑ 型で何を守るのか?
昨今、フロントエンドで静的型付けの需要が高まっていますね。各位導入事例も増えてきていることと思いますが、Vuex で TypeScript を導入する前に、なぜ型が必要なのかを再確認したいと思います。
Flux はモジュールの分散がつきものです。リリース当初は画面も確認しているし、小さいコードなのでミスには直ぐに気づきます。しかし、アプリケーションが肥大化し、様々なメンバーがコードを触る頃に問題が明るみに出ます。Flux は各所 State / Payload に強依存しており、肥大化したコードではその依存を把握しきれず、リファクタ不能に陥ります。
リリース前 | リリース後 |
---|---|
依存の分散 | 考慮漏れエラー |
運良くテストで検知出来れば良いのですが、全ての現場でテストが十分に揃っているとは限りません。また、ここの担保のためだけにテストを書くことも、開発工数に見合わなくなってきます。ここで、一つの問題を解決するモジュールを紹介します。
vuex-aggregate は commit / disptch の空振りと、それらの Payload を 推論型 で守ることを提供しています。README に記したとおり、まずは VSCode 等でリファクタのシミュレートをしてみてください。開発サーバーを立ち上げるまでもなく、直さなければいけない箇所にすぐに気づくことが出来ます。
プロダクションコードの健全性を保つために、リファクタは常に必要です。**「型はリファクタによる事故を防ぐためにある」**と言っても過言ではないと筆者は考えています。
🔧 mutation / action 名称をリファクタ
vuex-aggregate で提供している fromMutations
/ fromActions
で生成された commits / dispatches には推論型がかかっており、利用出来る commit / dispatch が予約されます。VSCodeなどではcommits.
まで入力するとコードヒントが出てきますね。これは型による副産物ですが、DXがかなり向上します。
開発者が定義した mutations / actions に proxy を通し、処理行程で推論型を導出する仕組みです。mutations / actions の関数名を変更すると、それを引き出して利用している commits / dispatches で「そんなものは無いよ」と怒ってくれます。これで臆することなく関数名を変更することが出来ます。
increment(state: State): void {
state.count++
}
上記 mutation を以下の様に変更してみます。いくつエラーが見つかったでしょうか? SFC でも反応していることが分かり、型無しの Vuex がいかに危険か察せるかと思います。
addCount(state: State): void {
state.count++
}
🛠️ Payload型 をリファクタ
mutations / actions の第2引数に定義した payload型をリファクタすることもあるかと思います。vuex-aggregate は TypeScript2.8 からの新機能(Type inference in conditional types)を利用しており、定義元の mutations / actions 第2引数型から commits / dispatches の payload型へと推論導出しています。これは、つい最近出来る様になった機能で、TypeScript ならではの芸です。
> 導出定義はこちら
setName(state: State, name: string): void {
state.name = name
}
上記 mutation の Payload型を以下の様に変更してみます。ここから生成された commit関数の payload型が追従し、エラーを確認することが出来ます。
setName(state: State, payload: { name: string }): void {
state.name = payload.name
}
他にも「number」にしてみるなど、挙動を確認してみてください。型の必要性を再確認できるかと思います。
🗑️ MutationTypes は書かない
example の通り、fromMutations
に mutations と namespace を与えることで、MutationTypes も自動生成されます。
import { fromMutations } from 'vuex-aggregate'
const namespace = 'counter'
const mutations = {
increment(state: State): void {
state.count++
},
setCount(state: State, count: number): void {
state.count = count
},
setName(state: State, name: string): void {
state.name = name
}
}
export const { commits, mutationTypes } = fromMutations(mutations, namespace)
各module の namespace がコンフリクトしない限り、それを配慮する必要もありません。もちろん、MutationType を import し mutation 関数名とすることも不要です。
namespaced: true ではあれど、modulesはネストせず、ファイル自体もネストせずにフラットに配備。ファイル名をそのまま namespace とすることを想定しています。
PR / issue お待ちしています
現状では推論の都合上、modules かつ namespaced という縛りがあり、各々敷かれている開発規約に抵触する恐れがあります。なにぶん Vuex 初心者のため、Vuexハードユーザーの皆さまから PR や issue を頂けますと幸いです。
また、筆者は普段 ReactRedux で開発しており、vuex-aggregate は redux-aggregate から発案した姉妹プロダクトです。vue ユーザーにも親しみ易い仕上がりになっていますので、是非試してみてください。