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
    • 利点
      • 表示で使うクラスを再定義する必要がなくなる場合がある
      • getterやメソッドを定義することで、表示ロジックやビジネスロジックを表現できる
    • 欠点
      • メソッドを使いたいならインスタンス化は必須であり、単なるオブジェクトからクラスのインスタンスへ変換する必要が出てきてしまう
  • interface
    • 利点
    • 欠点
      • 手続っぽい書き方になりがち
      • さらにクラスに変換するようなことになるので冗長になりがち。実装コストが高くなる

所感

  • 型安全はありがたい。
    • 関数の引数に違う型のものを渡そうとするとエラーになってくれるので嬉しい
    • 仕様変更でとあるフィールドを修正するとき、安心して修正できる。型を修正すれば、その型のフィールドを使ったものはほとんどエラーになるので、修正漏れしにくい。
    • もし型定義がなかったらオブジェクトにはどんなフィールドがあるか覚えてないといけない or 仕様ページ(wiki等)を見に行く手間があるので、ストレスフルだと推測できる。
  • ライブラリに型定義ファイルがないときは、自分で作る必要があるので面倒