thumbnail

Vue3でTypeScriptを使う方法【CompositionAPI】

Vue.jsでTypeScriptを使う方法、使う際に最低限知っておきたい事をまとめました。データや関数、算出プロパティなど、代表的な機能におけるTypeScriptの記述方法について一通り解説しています。ただし、今回説明する内容はVue.jsのバージョン3以降に登場した「CompositionAPI」を対象にした記述方法となります。そこでVue.js3をあまり知らない方のために、CompositionAPIの使い方にも最低限触れています。

CompositionAPIとは

Composition APIとは、Vue.js3から導入されたコンポーネントの新しい仕組みです。
これに対してVue2で一般的に使われていた、
data()、methods :{ }、computed:{ }を並べて利用したものをOptions APIと言います。

Composition APIはOptions APIよりもコードが綺麗に、よりシンプルに書けるようになります。
そして何より、TypeScriptとの親和性が非常に高いです。

本記事でも軽くまとめてありますが、CompositionAPI自体について詳しく知りたい方は是非公式ドキュメントをご参照ください。

Composition API | Vue.js

https://v3.ja.vuejs.org/api/composition-api.html#setup

Vue.jsでTypeScriptを使うための事前準備

Composition APIでTypeScriptを使う準備

雛形は以下のようになります。(Vue CLIやViteを使う場合、予め記述されている場合もあります。)

<script lang="ts">
import { defineComponent } from "vue"

export default defineComponent({
  setup() {}
})

</script>

TypeScriptを利用する際のポイントは以下の2点です。

  • scriptタグにlang属性の記述
  • defineComponent関数でsetup関数等を記述するオブジェクトを囲む

すぐ後の説明で、この記述方法を簡略化した「script setup構文」の説明をします。

setup関数について補足

setup関数をご存じの方は読み飛ばして下さい。

CompositionAPIでは、setup関数内にdataや関数、算出プロパティ、watchやライフライクルフック等すべてを書いていきます。これによって、コードが散らばらずに簡潔な記述ができたり、必要な処理だけを集めて外部ファイルに切り出してまとめること(コンポジション関数化)が出来る等、様々な恩恵を受けることが出来ます。

また、setup関数では、<template>側で使いたい変数や関数をreturn文に記述する必要があります。

setup() {
  // 変数や関数、computedやwatchの定義はここに書く
  const 変数A = ...
  const 関数A = () => {...}
  return {
    templateで使いたい変数A, 変数B, 関数Aなど
  }
}

ここまでの説明でピンと来なくても、この記事内にsetup関数の使い方のサンプルを一通り掲載しているので、記事を読み進めれば言わんとしてることが分かるかと思います。

更に簡略化された書式、<script setup>構文

前節で説明したような

  • ①defineComponent()の中で
  • ②更にsetup()で囲って、
  • ③templateで使いたい変数や関数をreturnする

…といった煩わしい記述を簡略化できる書き方が導入されました。
scriptタグにsetupを記述し<script lang="ts" setup>とすると、①~③の記述が一切不要になります。
scriptタグ直下に変数や関数、算出プロパティ等をそのまま記述することが可能になります。

<script lang="ts" setup>

//今までsetup(){}内に記述していたものをこの階層にそのまま記述できます。
//returnもいりません。

//変数
const hoge = ~

//関数
const hoge = () => {}
 
</script>

本記事のサンプルコードでは通常のsetup関数を使った記述をしていますが、これからVue3で開発するならばscript setup構文のほうが簡単に記述できますし、メリットも多いのでおすすめです。

このscript setup構文について詳しく知りたい方は、次の記事をご覧ください。

変数の書き方

従来(Options API)のdata()に相当します。変数の記法は変数の種類によって2つに分かれます。

オブジェクト以外の型: ref

オブジェクト以外の型はrefでラップします。次のようにインポートする必要があります。
import { ref } from "vue"

refについて

普通のJavaScriptで変数を定義する時と同じように、constやletで宣言できるようになりました。
基本的にリテラルはref()でラップする必要があり、これによってリアクティブになります。
(全く更新する予定のない変数はrefでラップする必要はありません。)
refという関数名の通り、変数は「値」では無く「参照」になります。

また、refをscript内で利用する場合は.valueを付けて利用します。(下記サンプル参照)
template側で利用する際.valueの指定はいりません。

TypeScriptでの書き方

TypeScriptでの書き方ですが、
TypeScriptが自動推論してくれる型(number型やstring型、boolean型等)には特別な記述はいりません。
明示的に型を付ける必要がある場合は、refにジェネリックを使います。
(自動推論してくれない場合や、自動推論される型より詳細な型宣言が必要な場合等が該当します。)

export default defineComponent({
  setup() {
    //型推論
    const name = ref('ryo') // Ref<string>型
        console.log(name.value)//.valueをつけて参照:string型になる
    
    //明示的な型付け
    const age = ref<number | string>(30) // Ref<number | string>型

    //明示的な型付け
    const element = ref<HTMLElement>() // Ref<HTMLElement | undefined>型
    
        return {
      name, age, element
    }
  }
})

T型の変数をrefでラップした変数の戻り値は、Ref<T>型になります。
例えば、string型の変数ならばRef<string>型になります。

setup関数内でrefの値を参照したい場合は.valueをつけます。
この時、Ref<T>型の変数をT型として参照できます。

  • 自動的に型推論してくれる
  • 明示的に型付けする場合はジェネリックで
  • Ref<T>型となる

オブジェクト: reactive

オブジェクトにはreactiveを使います。
import { reactive } from "vue"

reactiveについて

refと同様に、普通のJavaScriptを使う要領でconst宣言(あるいはlet)します。
オブジェクトはreactive()でラップすることでリアクティブになります。
refの使い方とあまり変わりませんが、値を参照する際に.valueの指定は必要ありません。

TypeScriptでの書き方

明示的に型を付けたい場合は、初めにオブジェクトの型をtypeやinterfaceで定義します。
その型を変数宣言の際にreactiveと一緒に使うのですが、方法は3つあります。

reactiveでTypeScriptを使う3つの方法

  1. ジェネリックを使う
  2. 明示的に型付けする
  3. 型アサーションを使う
//インターフェースの定義
interface Person {
  name: string
  age?: number
}

export default defineComponent({
  setup() {
    //方法1
    const person1 = reactive<Person>({
      name: 'taro',
      age: 22,
    })
    
    //方法2
    const person2: Person = reactive({
      name: 'jiro',
      age: 31
    })

    //方法3
    const person3 = reactive({
      name: 'hanako',
      age: 40
    }) as Person

    return {
      person1, person2, person3
    }
  }
})
  • まずはインターフェースを定義する
  • reativeオブジェクト宣言時にジェネリックでインターフェースを渡す

関数の書き方

Vue用の特別な記述は要らず、TypeScriptの普通の関数のように書きます。

setup() {
  const num = ref(0)

  //普通の関数と同じ記述
  const add = (n: number) => {
    num.value += n
  }

  return {
    num, add
  }
}

算出プロパティの書き方

computed()を使います。
import { computed } from "vue"
用途によって主に2つの書き方ができますが、computedで処理をラップするのは共通です。

computedについて

算出プロパティの用途による記述の違い

  1. getterのみを使う場合:関数を渡す
  2. getterとsetterをつかう場合:オブジェクトを渡す

getterのみ使用する場合は、算出プロパティ値をリターンする関数をcomputed()に渡します。

setterも使う場合は、get、setの2つのプロパティを持つオブジェクトをcomputed()に渡します。
それぞれのプロパティの値は関数です。

TypeScriptでの書き方

関数と同様に戻り値は自動的に推論してくれるので、基本的にVue用の特別な記述はいりません。
戻り値の型を自動で推論してくれない場合のみ、computedにジェネリックを使います。
setterを使う場合も使わない場合もcomputedの中には関数を使用していますが、この関数はTypeScriptで書きます。

setup() {
  const num = ref(0)
  
  //算出プロパティ
  const computedNum = computed(() => num.value + 5)

    //戻り値の型を推論してくれない場合
  const computedString = computed<string>(() => //何らかの処理)

  // getterとsetter
  const computedNum2 = computed({
    get: () => num.value + 30,
    set: (val: number) => num.value += val
  })

  return {
    computedNum, computedNum2
  }
}

ウォッチの書き方

watch()を使います。
import { watch } from "vue"
watch()の第一引数に監視対象、第二引数に処理を関数で渡します。
TypeScript用の特別な記述はいらず、この第二引数の関数をTypeScriptで書きます。

setup() {
  const num = ref(0)

  //ウォッチ
  watch(num, (oldValue, newValue) => {
    /* 処理 */
  })
}

watchには類似関数として、watchEffect、watchPostEffect、watchSyncEffectがあります。
watch及びこれらwatch系の関数の詳しい使い方は次の記事をご参照下さい。

ライフサイクルフックの書き方

onXXXX()の形で処理の関数をラップします。この関数をTypeScriptで書きます。これもインポートが必要です。

setup() {
  onMounted(() => console.log('mounted'))
  onBeforeUnmount(() => console.log('before unmount'))
}
  • created()とbeforeCreate()はsetup()に統合されたため廃止
  • destroyed()とbeforeDestroy()はunmounted()とbeforeUnmount()に変更

propsの書き方

propsプロパティ内での型付け

PropTypes<T>を使って具体的に型を指定できます。
import { PropTypes } from "vue"

もともとのpropsでもざっくりとした型を定義できましたが、PropType<T>でアサーションすることによって、より詳細に型を宣言できます。

export default defineComponent({
  props: {
    msg: String,  // プリミティブ
    txt: String as PropType< 'hoge' | 'fuga' >, // リテラル型の合併
    person: Object as PropType<Person>, //  Person型
    ary: Array as PropType<number[]>, //number[]型
    func: Function as PropType<(n : number) => void> // 関数型
  },
  setup() {}
})

きちんと型を指定すれば、validatorのチェック項目が減らせる場合がある(リテラル型の合併など)

propsについてもう少し踏み込んで説明します。

setup()内でのpropsの扱い

setup()内でpropsを扱う方法を説明します。

そもそも、setup()の第一引数はpropsを取ります。

setup(props) {}

このpropsはリアクティブです。
リアクティブを維持したままpropsをsetup()内で利用する時、いくつか注意することがあります。

toRef()を使う

toRef()でラップすることによってリアクティブを維持したままprops内のデータを扱えるようになります。

const prop = toRef(props, '使用したいprops名')

propsが複数ある場合はtoRefs()を使うと楽

propsが複数ある時、toRefs()でpropsをラップするとリアクティブを維持したまま分割代入が可能です。

const { props1, props2 } = toRefs(props) 

注意! propsの値は直接変更しない

toRefやtoRefsでpropsを参照できますが、propsの値をコンポーネント内で変更したい場合は、propsのコピーを変更するようにします。

//変更するならばRefではなくコピーを参照
const hoge = ref(props.プロパティA)

<script setup>構文を利用した際の記述方法

以下のようにdefinePropsを使います。インクルード不要です。

<script lang="ts" setup>

const props = defineProps({
  //この中は従来のpropsの記述と同じ
  props1: {
    type: String,
    required: true
  }
})
 
</script>

ジェネリックを使えば更に簡略化して記述できます。
詳しくは次の記事をご覧ください。

emitsの書き方

emitを使う場合は、setup()の第二引数ctxプロパティの中にあるemitを使います。
ctx.emit()のようにして利用します。

また、emitするイベント名をemits:["emitするイベント名"]として宣言します。この宣言はsetup()の中ではなく、外にsetup()と併記します。

emits: ['add'],
setup(props, ctx) {
  const add = () => ctx.emit('add', 5)
}
  • emitsで発行するイベントを定義することができる
  • emitsをObject形式で書くとバリデーションができる(今回は略)

emitsの具体的な使用例として、カスタムコンポーネントのv-modelにおけるemitの使い方を別記事でまとめています。

<script setup>構文を利用した際の記述方法

defineEmitsを使います。

<script lang="ts" setup>

const emit = defineEmits(['emit1', 'emit2'])
 
</script>

ProvideとInjectの書き方

少々長くなるので別の記事でまとめています。

テンプレート参照の書き方

ref()を使います。参照したいHTML要素にテンプレートでref属性をつけ、setup()内でrefを使ってアクセスしてreturnで返します。

<template>
  <p ref="msg">Hello</p>
</template>
setup() {
  const msg = ref<HTMLParagraphElement>()
  onMounted(() => console.log(msg.value?.textContent))

  return {
    msg
  }
}

プリミティブ型で使ったref()と同じものです

v-forで生成したDOMをテンプレート参照する方法は、次の記事を参照下さい。

Vuexの書き方

Vuexでもある程度TypeScriptを使用できます。
store.tsで使うステートの型の定義をします。

createStore()を使う際に、ステートの型をジェネリックで渡します。

export const store = createStore<State>({})

ゲッターやミューテーション、アクションは特別な記述の必要はなく、普通のTypeScriptの関数のように書けます

store.tsの例

nameとageの2つのステートのみを持つ簡単な例を挙げます。

import { createStore, Store, useStore as baseUseStore } from 'vuex'
import { InjectionKey } from 'vue'

// ステートの型を定義
export interface State {
  name: string
  age: number
}

// InjectionKeyの発行
export const key: InjectionKey<Store<State>> = Symbol()

// createStoreにStateをジェネリックで渡す
export const store = createStore<State>({
  state: {
    name: 'taro',
    age: 10
  },
  mutations: {
    setName(state, payload: string) {
      state.name = payload
    },
    setAge(state, payload: number) {
      state.age = payload
    },
   },
   actions: {
     changeName({ commit, state }, payload: string) {
       commit('setName', payload)
     },
     changeAngle({ commit, state }, payload: number ) {
       commit('setAge', payload)
     }
   }
})

// 外部で利用
export const useStore = () => baseUseStore(key)

インジェクションキー周りの記述はやや複雑ですが決り文句です。

  • ステートの型をinterfaceで定義
  • createStoreにステートの方をジェネリックで渡す
  • InjectionKeyの発行の際にもジェネリックを使う

storeを使う側は

import { useStore } from './store'

export default defineComponent({
  setup() {
    const store = useStore()
    console.log(store.state.age)
  }
})

app.mount(#app)の前にキーの登録 app.use(store, key)を忘れずに記述しましょう。

Vue Routerの書き方

ルーティング情報が入っている配列にジェネリックで型付けします。Vue CLIでプロジェクトを作成した場合は元から記述されています。

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "HOME",
    component: HOME,
  },
  {
    path: "/about",
    name: "About",
    component: () =>
      import(
        "../views/About.vue"
      ),
  },
]

この配列をcreateRouter()に渡します。

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
})

ナビゲーションガードについて知りたい方はこちらの記事をご覧ください。

まとめ

Composition APIの基本的な機能とTypeScriptの使い方をざっと見てきました。
箇条書き形式で紹介したので、目次から調べたい機能にジャンプできます。
また、広く浅くさらっただけなので紹介し切れていない部分も多いですが、別の機会に解説したいと思います。