Vue2系とTypescript周りの個人的な実装方針と所感
個人開発やプロダクト初期のVue 2系とTypescriptを用いたSPAの開発で得た知見のメモ。まだ試行錯誤中。
- 注意
- 適宜更新します。もし気になる点ございましたらご指摘いただけますとうれしいです。
- 自分が所属している団体を代表するような意見ではございません。
- 現在執筆中
小ネタ
リアクティブな連想配列
Vue 2系ではオブジェクトのプロパティを更新してもDOMが更新されずに困ることがある。 以下は、オブジェクトの参照自体がリアクティブなVueインスタンスのプロパティの場合、オブジェクトの任意のプロパティを更新した時に、オブジェクトの参照も都度変更してしまえばDOMの更新がされるだろうというやり方。オブジェクトのプロパティが多ければ処理効率は悪そう…
data(): { return { hashMap: Vue.observable<MyHashMap>(new MyHashMap({}) } },
class Xxx { constructor(public aId: number, public bId: number | undefined) {} } // aIdとbId毎のXxxを保持するコレクションオブジェクト // TODO: ジェネリクス使って再実装 class MyHashMap { // key: `${aId},${bId}` constructor(private state: { [key in string]: Xxx }) {} get = (aId: number, bId: number | undefined): Xxx | undefined => { const key = MyHashMap.createKeyBy(aId, bId) return this.state[key] } values = (): Xxx[] => Object.values(this.state) valuesBy = (aId: number, bId: number): Xxx[] => { return Object.values(this.state).filter( xxx => xxx.aId === aId && (xxx.bId || undefined) === bId ) } has = (aId: number, bId: number): boolean => { return this.get(aId, bId) != undefined } set = (aId: number, bId: number | undefined, xxx: Xxx): void => { // NOTE: リアクティブにするために新しくオブジェクトを生成して代入する. const newState = { ...this.state } const key = MyHashMap.createKeyBy(aId, bId) newState[key] = xxx this.state = newState } replace = (before: Xxx, after: Xxx): void => { this.set(before.aId, before.bId, after) } // NOTE: このクラスのstateのkeyを生成する時は必ずこのメソッドを使うようにする. static createKeyBy = (aId: number, bId: number | undefined): string => { return `${aId},${bId}` } // NOTE: このクラスのstateのkeyからkeyの構成要素を取得したい時は必ずこのメソッドを使うようにする. // return [aId, bId] static fromKey = (key: string): [number, number | undefined] => { const maybeIds = key.split(',') if (maybeIds.length === 3) { return [Number(maybeIds[0]), maybeIds[1] == 'undefined' ? undefined : Number(maybeIds[1])] } console.log(`Error in MyHashMap#fromKey: ${maybeIds}`) return [NaN, NaN] } }
子コンポーネントへのメソッドの渡し方は$emitよりもpropを使う
- 理由
- $emitは文字列でメソッド名を扱ったり、引数が可変長で型がAnyだったりで型安全ではないため
- propであれば型安全に扱えるため
↓以下、子コンポーネント側の例
props: { inputFunc: { type: Function as PropType<(value: string) => void>, required: true, }, }
vuexよりはstoreパターンをなるべく使う
- vue.observableを使ったシンプルなstoreパターンを採用する
- オブジェクトをリアクティブにするため(storeの値が変わったとき、変更をDOMにも反映させるようにするため)
- シンプルなstoreパターンで事足りるため。
今のところvuexはあまり利用していない。vuexを利用していない理由としては - そもそも小さなプロダクトには、vuexがは向いていないから - 型安全ではないから(型安全にしようとすると、デコレータをたくさん使う…)
nullよりもundefinedをなるべく使う
- 理由
- Optional Chainingではnullではなくundefinedを扱うため
扱うUIフレームワークに合わせるのもいいかもしれない。Vuetifyではnullよりもundefinedを扱っているケースが多く見受けられる
axiosに渡すAPIのリクエストやレスポンスを表すオブジェクトの定義はclassかinterfaceか
classを定義すれば同名のinterfaceも定義したようなものなので、インスタンス化してなくてもメソッドが使えないだけでプロパティは参照できる。 そのため、axiosで受け取る・axiosで渡すオブジェクトの定義はclassかinterfacelかの選択肢がある。
- class
- interface
- 利点
- 表示ロジックやビジネスロジックとは隔離されているので、やや変更に強い
- 欠点
- 手続っぽい書き方になりがち
- さらにクラスに変換するようなことになるので冗長になりがち。実装コストが高くなる
- 利点
所感
- 型安全はありがたい。
- 関数の引数に違う型のものを渡そうとするとエラーになってくれるので嬉しい
- 仕様変更でとあるフィールドを修正するとき、安心して修正できる。型を修正すれば、その型のフィールドを使ったものはほとんどエラーになるので、修正漏れしにくい。
- もし型定義がなかったらオブジェクトにはどんなフィールドがあるか覚えてないといけない or 仕様ページ(wiki等)を見に行く手間があるので、ストレスフルだと推測できる。
- ライブラリに型定義ファイルがないときは、自分で作る必要があるので面倒