Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

Vue2 (nuxt2) で TypeScript の型を守る Tips

こんにちは、ブロックチェーンチームの id:d-kimsuon です

Vue2 では TypeScript がサポートされており、公式の TypeScript のサポートのドキュメント に従うことで TypeScript で書いていくことができます

しかし、素直に書いていくと any 型になってしまったり、実際とは異なる型付けになってしまうポイントがあります

この記事では TypeScript で Vue2 (Option API) を書くときに型を厳格にしやすい書き方等を紹介します

省略可能な props には明示的に PropType<型 | undefined> する

Vue の props では required: false または default に値を設定することで、省略可能なプロパティを宣言することができます

型安全な例

PropType で明示的に型を指定することで、型安全に参照することができます

Vue.extend({
  props: {
    foo: {
      type: String as PropType<string | undefined>,
      required: false
    },
  }
})

型安全でない例

次のような素直な実装でも省略可能な props は実現できますが、型付けが厳格でなくなります

fooは、いずれも string | undefined に型推論されてほしいですが、 string に推論されてしまいます

Vue.extend({
  props: {
    foo: {
      type: String,
      required: false
    },
  }
})
Vue.extend({
  props: {
    foo: {
        type: String,
        default: undefined
    },
  }
})

props の型でリテラルに絞っている時、 default には as const をつける

PropType<'ok' | 'ng'> 等で受け取る型をリテラルで絞っていても default 値が指定されていると string に丸められてしまいます

as const をデフォルト値につけることで PropType の型でちゃんと推論してくれるようになります

型安全な例

Vue.extend({
  props: {
    foo: {
      type: String as PropType<'x' | 'y' | 'z'>,
      default: 'x' as const  // これがないと string に推論される
    }
  }
})

型安全でない例

Vue.extend({
  props: {
    foo: {
      type: String as PropType<'x' | 'y' | 'z'>,
      default: 'x' // as constがないため、string に推論される
    }
  }
})

data の型を as で上書きせず、型注釈を書く

data はミュータブルなデータなので初期値からの推論が適切でないときがあります

そういうとき、as で型を上書きする手法が使われがちですが型と値がズレる原因になります

型安全な例

data メソッドの戻り値の型を書くことで安全に data の型を宣言できます

Vue.extend({
  data(): { foo: { hoge: string } } {
    return {
      foo: { hoge: "hello" }
    }
  }
})

型安全でない例

Vue.extend({
  data() {
    return {
      foo: null as null | { hoge: string }
    }
  }
})

as だと型注釈とは異なり、型情報を上書きします。値をその型の変数に束縛できるかを緩くしか検証しないので型と値が一致しなくなる原因になります

例えば以下は型エラーがでない、型と値がズレる例です

// asで型を書くダメな例
Vue.extend({
  data() {
    return {
      foo: {} as { hoge: string }
    }
  }
})

型注釈で書いていれば {} は 型 { hoge: string } に割り当てることはできませんが、as で書いていたために代入できてしまっています

既存のコンポーネントに data を追加する際に型注釈が書いてないと as で対応しがちなのかなと思っているので、最初から型注釈を書いておくのが良いのかなと思っています

asyncData の型は推論ではなく Promise<Partial<Data>> を使う

※ nuxt の話題で、v2.16.0で修正されている のでそれより古いバージョンの前提です

asyncData にだけ書いたデータは this.serverData が生えず型安全に参照することができません

ですので、data の型に SSR から取得するデータの型も初期値とセットで宣言しておくのがオススメです

型安全な例

// ローカルステートの型を初期化状態とセットで宣言して
type Data = {
  clientData: string,
  serverData: string | undefined /* 未ロードを undefined にする */,
}

Vue.extend({
  // asyncData は Partial<Data> に注釈をつける
  asyncData: async (): Promise<Partial<Data>> => {
    return {
      serverData: 'hogehoge'
    }
  },
  data(): Data {
    return {
      clientData: 'hello',
      serverData: undefined   // 初期化
    }
  }
})

型安全でない例

type Data = {
  clientData: string,
}

Vue.extend({
  asyncData: async () => {
    return {
      serverData: 'hogehoge'
    }
  },
  data(): Data {
    return {
      clientData: 'hello',
    }
  },
  methods: {
    foo() {
      console.log(this.serverData)  // 型エラー
    }
  }
})

watch には型注釈を明示する

watch 対象のプロパティをの型を推論してほしいですが、実際には入ってくれません

型安全な例

やや手間ですが型注釈を明示してあげることで、型安全に利用できます

Vue.extend({
  props: {
    foo: String
  },
  watch: {
    hoge(nextValue: string, prevValue: string): void {
      // 明示する
    }
  }
})

型安全でない例

Vue.extend({
  props: {
    hoge: String
  },
  watch: {
    hoge(nextValue, prevValue): void {
      // nextValue と prevValue は string に推論されてほしいけど any になる
    }
  }
})

ref からメソッドを呼ぶ

ref を使って子コンポーネントのメソッドを呼び出すとき、型安全にメソッドを呼ぶことはできません

子コンポーネントのメソッドを呼ぶこと自体避けたほうが良いかもしれませんが、止むをえず呼ぶ場合は以下のような型の Utility を準備しておくことで型安全に呼び出すことができます

type ComponentMethods<Comp> = Comp extends ExtendedVue<
  Vue,
  unknown,
  infer I,
  unknown,
  unknown
>
  ? I
  : never;
type TypedVueRef<Comp extends VueConstructor> = Vue & ComponentMethods<Comp>;

以下のように利用します

const ref = this.$refs['v-comp'] as unknown as TypedVueRef<typeof VComp>

最後に

この記事ではVue2系で型安全性を守りやすい書き方を紹介しました

Vue で型が厳格にならず困っている方はぜひお試しください!