💡

Vue3の変更の概要とTS実装

7 min read

以前にVue2を使った際に、特にTypeScriptの対応状況がしんどいと感じました、
しかし、Vue3になり、TypeScriptの対応が厚くなったと公式で発表していたので、
Vue3の機能的な変更点を簡単におさえつつ、TypeScriptの書き味を試してみます。

変更点

Composition API

概要

v3での最も大きな機能追加。
その名の通り、PropsやContextよりComponent全体で使用可能な変数を返す。
React Hooksに近い記述で動的に参照可能な変数や関数を作成する。

目的

従来のVue Componentでは、同じ関心事に関するものであっても、
下記のように、変数や関数が様々なオプションに分かれてしまう。

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [],
      filters: { ... },
      searchQuery: ''
    }
  },
  computed: {
    filteredRepositories () { ... },
    repositoriesMatchingSearchQuery () { ... },
  },
  watch: {
    user: 'getUserRepositories'
  },
  methods: {
    getUserRepositories () {
      // using `this.user` to fetch user repositories
    },
    updateFilters () { ... },
  },
  mounted () {
    this.getUserRepositories()
  }
}

その結果、Componentが大きくなるほど、1つの関心に関する処理を読み書きするにも、
様々な箇所に飛ばなければならず、可読性や生産性を下げるという問題があった。
そこで1箇所に同じ関心事に関する処理をまとめるために作成された。

コード例

import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'

export default {
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup (props) {
    // using `toRefs` to create a Reactive Reference to the `user` property of props
    const { user } = toRefs(props)

    const repositories = ref([])
    const getUserRepositories = async () => {
      // update `props.user` to `user.value` to access the Reference value
      repositories.value = await fetchUserRepositories(user.value)
    }

    onMounted(getUserRepositories)

    // set a watcher on the Reactive Reference to user prop
    watch(user, getUserRepositories)

    const searchQuery = ref('')
    const repositoriesMatchingSearchQuery = computed(() => {
      return repositories.value.filter(
        repository => repository.name.includes(searchQuery.value)
      )
    })

    return {
      repositories,
      getUserRepositories,
      searchQuery,
      repositoriesMatchingSearchQuery
    }
  }
}
  • setup関数で返されたものがComponent全体で使用可能となる
  • toRefsrefを使用することで、値は動的な参照を持ち、Component内でhoge.valueより変更後の値を動的に参照可能となる
  • ライフサイクルもサポートされており、onMountedはマウント後に実行する関数を指定する
  • computedを使用することで、computed valueも作成可能

Teleport

概要

Vue Component内のdomの一部を、レンダリング時に指定したdomの直下に描画する。
全画面のモーダルをbody直下に描画したい際などに使用する。

コード例

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

Fragments

概要

ルートに複数のdomを置ける

コード例

v2

<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>

v3

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

Emits Component Option

概要

新たに追加されたオプション。
Componentがどのような働きをするかを記述するために、ここにemitする全てのカスタムイベントを記述することが推奨される。

コード例

app.component('custom-form', {
  emits: {
    // No validation
    click: null,

    // Validate submit event
    submit: ({ email, password }) => {
      if (email && password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  },
  methods: {
    submitForm() {
      this.$emit('submit', { email, password })
    }
  }
})
  • emits: ["click", "submit"] のように配列での指定も可能
  • オブジェクトで関数を指定すると、バリデータとして働く

Custom Render(createRender)

概要

その名の通り、カスタムレンダラーを作成する。
これらのオプションを使用し、レンダリング時に特定の処理を入れられる。

コード例

import { createRenderer } from '@vue/runtime-core'
import { nodeOps } from '@vue/runtime-dom'

const { render, createApp } = createRenderer({
  ...nodeOps,
  insert: () => {},
})

// `render` is the low-level API
// `createApp` returns an app instance with configurable context shared
// by the entire app tree.
export { render, createApp }

export * from '@vue/runtime-core'

TypeScript対応

ここからが本題のTS対応です。

Component宣言時の注意

まずはVue3でTSを使用する際の注意点をおさえます。

1. scriptタグの言語をtsに指定

<script lang="ts">
  ...
</script>

2. defineComponent関数の使用

Component内で正しく型推論させるためには、従来のクラスではなく、
この関数を使用し、Componentを作成しなければならない。

import { defineComponent } from 'vue'

const Component = defineComponent({
  // type inference enabled
})

変更点

Vue3でTSのサポートが厚くなったという点に関して調べてみると、
Vue3は全てTSで書き直されているため、TSが使いやすくなったという文脈らしく、
目に見える利点は公式ドキュメントでも言及されていなかった。
ただ、Composition API内にロジックがまとまるため、TSの恩恵を受ける部分は大きくなりそうだと感じる。

実際に書いてみて

そこで実際の書き味を試してみるために、vue-cliを使用し、Vue+TSのプロジェクトを作成し、実際に書いてみた。
その結果、型推論が効かない箇所や純粋なTSの書き方をできないため、以前のしんどさは解消されてないように感じた。
以下に特にしんどいと感じた箇所を書いていく。

しんどいと感じた箇所

template内で型が効かない

ここが一番しんどいと感じる箇所だが、template内では当然型が効かない。
例えば以下のようなコードがあった場合、template内では下記のような状態になる。

  • readersNumberにはnumber型が効かない
  • bookと入力した際に、titleをサジェストしない
<template>
  <div>{{ readersNumber }} {{ book.title }}</div>
</template>

<script lang="ts">
import { ref, reactive, defineComponent } from 'vue'

export default defineComponent({
  setup() {
    const readersNumber = ref(0)
    const book = reactive({ title: 'Vue 3 Guide' })

    // expose to template
    return {
      readersNumber,
      book
    }
  }
})
</script>

ある程度複雑な配列やオブジェクトを渡すことやtemplate内の条件分岐は当然想定されるので、
その状況で型が効かないのはバグを生み出しやすい。

Vueのシンタックス上の理由で、型システムの使用を強制することがある

Vueのシンタックス上の理由で、型推論が効かないケースがある。
その場合、本来使用しなくていい箇所で使用することを強制するような作りになっている。

例えば、computedプロパティでは、型アノテーションが必須になっている。

import { defineComponent } from 'vue'

const Component = defineComponent({
  data() {
    return {
      message: 'Hello!'
    }
  },
  computed: {
    // needs an annotation
    greeting(): string {
      return this.message + '!'
    },
    // in a computed with a setter, getter needs to be annotated
    greetingUppercased: {
      get(): string {
        return this.greeting.toUpperCase();
      },
      set(newValue: string) {
        this.message = newValue.toUpperCase();
      },
    },
  }
})

まとめ

Vueは用意されている様々なメソッドを使用するだけで、簡潔な記述でComponentを作成できるという
手軽さが利点にあげられることが多いように思います。
しかし、様々なメソッドがあらかじめ用意されているために、TSを使用する際の制約やエッジケースの対処に困る状況が生まれていることも事実だと思います。
スキルセットなどチームの状況に合わせて、慎重に採用を決める必要があります。