Vue3でTypeScript実践

TypeScript の動作確認

TypeScript を設定しながら動作確認を行っていきますが基本的に行うことはいずれも共通で最初に型を定義して設定(関数であれば引数に入る値への型、また関数を実行した後に戻り値の型)し、その型と異なる型を入力しようとしたり異なる型が戻るようなコードを記述するとエラーメッセージで教えてくれるといったものです。つまり実行時にエラーを見つけるのではなくコードを記述する際にエラーを見つけることで本番環境でのエラーの発生を抑えることができます。

変数の型の設定方法

TypeScript では変数を定義する際にその変数にどの型(タイプ)を持った値が入るのか設定することができます。頻繁に利用する型として String, Number, Array, Boolean, Function, Object などがあります。そのほかにも any 型や void 型、リテラル型などがありますが。まずは先に出てきた 6 つの型(タイプ)を利用するために TypeScript を使ってどのように型の指定を行うのか確認していきます。

JavaScript の場合は変数を定義して各変数に値を設定するだけです。特に型に関して明示的に設定を行うことはありません。そのため最初に文字列を設定し後ほど数値を設定するといったことが可能です。

let city = 'Tokyo';
let temp = 20;
let loading = true;
//文字列、数値など型を気にしない
city = 40;

TypeScript で型の指定を行う場合は変数の名前の後ろに:(コロン)をつけて型を指定します。変数 city は文字列が入るので string 型, temp は数字が入るので number 型, loading は true か false の値のいずれの値しか入らないので boolean 型を設定します。型を定義した後に異なる型を設定しようとするとエラーメッセージが表示されます。

let city: string = 'Tokyo';
let temp: number = 20;
let loading: boolean = true;
// 下記はエラー
city = 20;

次に配列を確認します。これまでの流れからすると配列は下記のように記述できそうな感じがします。

let cities: array = ['Tokyo', 'Paris', 'London'];

配列の場合は要素の中に入る値の型の指定を行います配列の要素の型がすべて string なので下記のように記述します。配列の記述方法には 2 つあり、2 つ目はジェネリクスを利用しています。

let cities: string[] = ['Tokyo', 'Paris', 'London'];
or;
let cities;
Array = ['Tokyo', 'Paris', 'London'];

string ではなく boolean の配列の場合は下記のように記述します。

let a: boolean[] = [true, false, false];
or;
let a: Array = [true, false, false];

関数の場合はどのように型を設定するか確認するために引数に入れた 2 つの値を足してその結果を戻す関数 add を使います。関数を見ると型を指定できそうな箇所が 2 つあることがわかります。1 つは引数、2 つ目は関数から返される値です。

function add(a, b) {
  return a + b;
}

TypeScript で型を指定すると下記のように記述することができます。add 関数はシンプルなのであまり有益性は感じないかもしれませんが内部の処理が複雑な場合は add 関数は a, b の数値を入れると数値が戻ってくると型の指定を見ればわかるので add 関数を再利用する際に役立てることができます。TypeScript を利用するとコードの可読性がよくなるといわれているのはこのことからもわかります。その反面 TypeScript を理解せずコードを見ると型がただの記号にしか見えず理解するのが困難になります。

function add(a: number,b: number): number{
  return a + b;
}
//アロー関数
const add = (a:number, b:number):number{
  return a + b;
}

引数と関数の戻り値に型を設定することがわかりましたが関数自体にも型を設定することができます。関数名の右側に:で関数の型を設定しています。(a: number, b: number) => number の部分が関数に設定した型です。

const add: (a: number, b: number) => number = (
  a: number,
  b: number
): number => {
  return a + b;
};

オブジェクトを持つ変数 user に型を設定することでオブジェクトへの型の設定を確認します。

const user = {
  id: 100,
  name: 'John Doe',
};

オブジェクトを持つ user にはこれまで出てきた string 型, number 型を直接設定することはできません。その代わりにオブジェクトが持つプロパティに対して型を設定します。オブジェクトとは異なりプロパティと値毎に”,“(カンマ)を設定するのではなく”;“セミコロンを利用します。セミコロンはプロパティと型のペア毎に改行する場合は必須ではありませんが1行に型の設定を記述したい場合は必須となります。

const user: {
  id: number,
  name: string,
} = {
  id: 100,
  name: 'John Doe',
};

1 行に並べるのは以下のような場合です。

const user: { id: number, name: string } = {
  id: 100,
  name: 'John Doe',
};

computed プロパティ

TypeScript で使われる基本な型の説明が終わったので Vue 特有の機能である computed プロパティでの型の設定について確認していきます。

TypeScript がどのように computed プロパティに影響するかを確認するために data プロパティに設定した文字数を表示するため HelloWorld.vue ファイルに computed プロパティ getLength を追加します。

computed:{
  getLength() {
    return this.msg.length
  }
}

TypeScript を利用している場合は getLength メソッドの戻り値の型を明示的に設定することができます。msg の文字列の長さを計算する関数なので戻り値は数値となるため設定する型は number になります。下記のように:(コロン)の横に型を設定することで関数の戻り値の型を設定することができます。

computed:{
  getLength(): number {
    return this.msg.length
  }
}

戻り値の型を number に設定した後に動作確認のため this.msg.length から length を一時的に削除してください。削除することにより getLength の戻り値が数字から文字列になるためエラーが発生します。

Type “string”は type “number”に割り当てることができないというメッセージが表示されます。

型のエラー

TypeScript ではエディターの機能によりファイルを保存する前にメッセージが表示されるのでコードの間違いにすぐに気づくことができます。

computed プロパティへの型の設定方法と TypeScript で設定した型と getLength で戻される値の型が一致しない場合にどのようなエラーが表示されるか確認することができました。

型推論

data プロパティで msg の型を指定していない場合でもエラーが出力されず問題なく動作します。これは data プロパティの msg に文字列を設定した時に TypeScript が型を自動で string と判断してくれるためです。これを型推論と呼び、msg のように明確に型がわかる場合は型を指定する必要がありません。

msg の型を string と判断しているので、methods を利用して msg の値を数値に変更しようとするとエラーメッセージが表示されます。

methods: {
  changeMsg() {
    this.meg = 3;
  },
},

Type “number”は type “string”に割り当てることができないというメッセージが表示されます。

型推論の動作確認

changeMsg メソッドに引数がある場合を確認します。引数の msg が string の場合は data プロパティの型も string なので問題もなく処理ができます。

  methods:{
    changeMsg(msg: string) {
      this.msg = msg
    }
  },

引数の msg に string とは異なる型である number を設定した場合には、Type ‘number’ is not assignable to type ‘string’のメッセージが表示されます。

  methods:{
    changeMsg(msg: number) {
      this.msg = msg
    }
  },

data プロパティへの明示的な型設定

data プロパティに msg を追加し文字列を設定すると型推論により自動に型が設定されました。

明示的に型を設定したい場合は下記のように設定することができます。

data(): {
  msg: string;
} {
  return {
    msg: 'Hello TypeScript',
  };
},

as を利用して下記のように設定することもできます。as を利用して型を設定する方法は型アサーションと呼ばれます。

data() {
  return {
    msg: 'Hello TypeScript' as string,
  };
},

設定した値(下記では文字列)とは異なる型を設定した場合はエラーメッセージが表示されます。

data() {
  return {
    msg: 'Hello TypeScript' as number,
  };
},
異なる型を設定した場合

msg に文字列だけではなく数値も入れたいという場合は union 型で記述することができます。この場合は msg の値が文字列でも数値でもエラーメッセージは表示されません。

data() {
  return {
    msg: 'Hello TypeScript' as number | string,
  };
},

changeMsg メソッドの引数にも union 型を設定することができます。data プロパティに number と string を設定した後に changeMsg の引数に必ず number と string を設定する必要があるのではなく string を持つ値でも number を持つ値でも設定することができます。

 methods:{
    changeMsg(msg: number | string) {
      this.msg = msg
    }
  },

メソッドの型

computed プロパティと同様にメソッドの戻り値に明示的に型を設定することができます。メソッドに引数がある場合は引数にも型を設定することができます。

型推論により型が自動で設定されるため型の設定を行わなくてもエラーメッセージは表示されていません。メソッドの名前にカーソルを合わせると戻り値に何もない時に設定することができる void 型が設定されています。

methods: {
  changeMsg() {
    this.msg = 'Hello World';
  },
},
型の確認

メソッドの場合は引数を設定することができます。引数に型を設定していない場合は any が設定されます。

引数に型の指定がない場合

any はどのような値でも設定できることから型の間違いを判定することができなくなりバグに繋がる可能性があるので引数には適切な型を設定します。引数の msg に適切な型である string 型を設定します。

メソッドの型の確認

changeMsg で return で msg を戻すように変更を行います。

methods: {
  changeMsg(msg: string) {
    this.msg = msg;
    return msg;
  },
},

再度メソッドにカーソルを合わせるとメソッドの戻り値が void から string に変わっていることがわかります。

メソッドの戻り値の型の確認

メソッドの戻り値に明示的に型を設定することもできます。

methods: {
  changeMsg(msg: string): string {
    this.msg = msg;
    return msg;
  },
},

メソッドによって戻される型と戻り値の型を異なる型の number 型に変更するとメッセージが表示されます。戻り値に設定した number ではなく return msg にメッセージ表示されます。コードの中身から number 型が戻り値にならないことがわかるためエラーとなります。

methods: {
  changeMsg(msg: string): number {
    this.msg = msg;
    return msg;
  },
},
メソッドの戻り値のエラー

データオブジェクトへの型設定

オブジェクトのような複数のプロパティを持つ場合は型の宣言に interface または type を利用することができます。interface はオブジェクトの設計図のようなもので実際に値は入っていませんが、interface を見ることでどのようなプロパティと型でオブジェクトが構成されているかがわかります。

interface を利用して型の設定方法を確認します。まず interface で Book 型を宣言します。Book 型には title, author, year の 3 つのプロパティが含まれており string と number の型を持っています。

interface Book {
  title: string;
  author: string;
  year: number;
}

データプロパティ book を追加します。追加しましたが型の設定は何も行っていません。この状態では追加した interface の Book との関連は何もありません。

export default defineComponent({
  name: 'HelloWorld',
  data() {
    return {
      book: {
        title: 'Vue 3 Guide',
        author: 'Vue Team',
        year: 2020,
      },
    };
  },
});

追加した book を下記のように template タグに設定しても問題なく表示されます。

<template>
  <h1>{{ book.title }}/{{ book.author }}/{{ book.year }}</h1>
</template>
book の中身を表示

メソッドを利用して year を 2020 の数値から文字列に変更してみましょう。型推論により book の year は数値の型が設定されているので文字列に変更しようとするとエラーが発生します。

エラーメッセージが表示

しかし、book については型を設定していないため初期値を文字列にすると変更は可能となります。初期値を文字列に変更すると今度は数値に変更しようとするとエラーが発生します。

export default defineComponent({
  name: 'HelloWorld',
  data() {
    return {
      book: {
        title: 'Vue 3 Guide',
        author: 'Vue Team',
        year: '2020',
      },
    };
  },
  methods: {
    changeYear() {
      this.book.year = '2021';
    },
  },
});

ここまでは interface で定義した Book 型を利用していなかったので interface を利用していない場合の型に対する処理(型推論)を理解することできました。次は interface を利用し、interface を設定しない場合との違いを理解していきましょう。データプロパティに型を明示的に設定したいのでデータプロパティに型を設定します。

interface Book {
  title: string;
  author: string;
  year: number;
}

export default defineComponent({
  data():{
    book:Book
  } {
    return {
      book: {
        title: 'Vue 3 Guide',
        author: 'Vue Team',
        year: 2020,
      } as Book,
    };
  },
});

as を利用して Book の型を設定することもできます。

data() {
  return {
    book: {
      title: 'Vue 3 Guide',
      author: 'Vue Team',
      year: 2020
    } as Book
  }
},

book に対して interface で定義した Book 型が設定されたのでもし year の 2020 を文字列に変更しようとするとエラーメッセージが表示されます。Book 型を設定することで初期値を設定する際にエラーメッセージが表示されるため誤った初期値を設定することがなくなります。

エラーメッセージ

初期値を設定後にメソッドを使って year に文字列を設定しようとするとエラーが発生します。数値の場合は変更を行っても型が変わらないためエラーは発生しません。

型エイリアス

interface 以外にもオブジェクトに型を設定したい場合に利用できる type があります。type は型エイリアスと呼ばれエイリアスという名前がついているように型に別名をつける時に利用することができます。type を利用することで number の型に Year という別名をつけています。type で作成した別名の Year を使って型を設定することができます。下記では Year という名前の number 型の別名をつけることで birthday 変数の型に Year 型を設定できるようになります。Year を設定することで number 型を設定することになります。

type Year = number;
const birthday: Year = 2000;

type は interface と同様にオブジェクトに対して型を設定することができます。interface とは定義の記述方法が似ていますが type では=(イコール)が使われたり、最後の閉じるカーリーブレースの後ろに:(コロン)がつきます。

type Book = {
  title: string,
  author: string,
  year: number,
};

type は下記のように決められた複数の値を持つ変数の union 型設定に利用することができます。inject には pfizer, moderna, astrazeneca 以外を入れようとするとエラーになります。

Type VACCINE = 'pfizer' | 'moderna' | 'astrazeneca';
const inject: VACCINE = 'pfizer'

interface や type の違いなどは下記の文書に記述しているのでぜひ参考にしてみてください。

props の場合

data プロパティのオブジェクトの設定方法が確認できたので次は props の場合はどのように型を設定していくのか確認していきましょう。

props については Vue 自身にも props で型の設定を行う機能を持っています。まず Vue が持つ props での型の設定方法を利用して動作確認を行います。

HelloWorld.vue で props の msg を設定します。型は String で設定します。

<template>
    <h1>{{ msg }}</h1>
</template>

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

export default defineComponent({
  name: 'HelloWorld',
  props:{
    msg:String
  }
});
</script>

App.vue で props の msg に Hello TypeScript を設定します。

<template>
  <HelloWorld msg="Hello TypeScript" />
</template>

ブラウザで確認すると Hello TypeScript が表示されます。

props を利用して表示

App.vue 側から msg に数値を入れるとブラウザのコンソールにエラーメッセージが表示されます。ブラウザのデベロッパーツールを利用して確認してください。

<template>
  <HelloWorld :msg="100" />
</template>
コンソールにエラメッセージ表示

実行した時に型に問題があることがわかります。

数値で props を渡す場合は:(コロン)を忘れずに付与してください。付与しない場合は文字列として渡されます。

Vue.js が持つ型設定ではなく TypeScript で props の型を設定する場合は PropType を import し PropType のジャネリクスを使って型を設定します。

<script lang="ts">
import { defineComponent, PropType } from 'vue';

export default defineComponent({
  name: 'HelloWorld',
  props:{
    msg:{
      type: String as PropType<string>,
    }
  }
});
</script>

App.vue ファイルから文字列ではなく数値を渡そうとすると props の PropType で設定した型と一致しないためエラーメッセージが表示されます。先ほどのように Vue の機能のみで型を設定した時のように動作確認をして初めてエラーが表示されるということはありません。

props で受け取る値が数値である場合は下記のように設定を行うことができます。

type: Number as PropType<number>,

オブジェクトの props

Props が先ほどのように String や Number ではなく Object の場合は interface を利用して型の設定を行うことができます。

props を受け取る側の HelloWorld.vue で受け取る props データの interface を作成します。firstName, lastName, age の 3 つのプロパティを持ちます。

interface User {
  firstName: string;
  lastName: string;
  age: number;
}

props の名前は user で PropType を利用して型を設定します。Vue.js が持つ props の機能で require も設定しています。required は親から渡される props が必須の場合に設定を行います。

props: {
  user: {
    type: Object as PropType<User>,
    required: true,
  }
},

受け取った props は computed プロパティを利用してブラウザに表示するため、fullName という関数を追加し、戻り値の型は string に設定します。

computed: {
  fullName(): string {
    return this.user.firstName + ' ' + this.user.lastName
  }
}

template タグの中で設定した computed プロパティ fullName を表示させます。一緒に this.user.age も表示させます。

<template>
  <h1>{{ fullName }} {{ this.user.age }}</h1>
</template>

props を渡す親側のコンポーネント App.vue の設定を行います。data プロパティで person を設定し、firstName, lastName, age を設定しています。person データを computed プロパティ user に設定します。

data(){
  return {
    person:{
      firstName: 'John',
      lastName: 'doe',
      age: 30,
    }
  }
},
computed:{
  user(): object {
    return this.person;
  }
}

template タグの中で v-bind を利用して computed プロパティの user オブジェクトを HelloWorld コンポーネントに渡します。

<template>
  <HelloWorld :user="user" />
</template>

ブラウザで確認するとユーザ名と age を確認することができます。

ユーザ名と age が表示

TypeScript で型の設定を行っているので、App.vue 側から渡すデータの型が受け取る側の設定した型と異なる場合にデータを送信するとエラーメッセージが表示されます。下記では age を文字列に変更しています。コンパイル時に Type ‘string’ is not assignable to type ‘number’.というエラーが表示されます。

  person:{
      firstName: 'John',
      lastName: 'doe',
      age: '30',
    }

コンパイルは npm run serve を実行している中でファイルの保存を行うと自動で行われます。

Interface を Export する

HelloWorld で作成した interface は Export を行うことで親コンポーネントでもその Interface を利用することができます。

別のコンポーネントファイルで記述した interface だから利用できるわけではなく別ファイルで interface を定義して export を行えば他のコンポーネントで import することができます。

親と子のコンポーネントで同じ interface の定義を共有することで親コンポーネントで初期値を設定する際にエディターのエラーメッセージで型の間違いを確認することができます。

export を interface の前に追加します。

export interface User {
  firstName: string;
  lastName: string;
  age: number;
}

親コンポーネントの App.vue で export した Interface を import します。これで App.vue でも Interface の User を設定することができます。

import HelloWorld, { User } from './components/HelloWorld.vue';

Interface の User を computed プロパティの user で利用します。

computed:{
  user(): User {
    return this.person;
  }
}

この状態で初期値の age の値を数値から文字列に変更すると下記のエラーメッセージが表示されます。

エディターのエラーメッセージ

computed プロパティの戻り値に User を設定しましたが、今回は data プロパティのオブジェクトも同じ型なので data プロパティにも型を設定することができます。

data(){
  return {
    person:{
      firstName: 'John',
      lastName: 'doe',
      age: 30,
    } as User
  }
},

firstName、lastName を数値、age を文字列に変更するとすぐにエラーが表示されます。

Vue の Options API を利用した場合の Vue3 環境における TypeScript の設定例でしたが Vue のアプリケーションを構築する上で必須の要素である data, props, computed, methods での TypeScript を利用方法を確認することができました。

Composition API と TypeScript

これまでは Options API を利用して TypeScript の設定方法を確認してきました。Vue3 から Composition API を利用してコードを記述することができます。ここからは Composition API を利用した場合の TypeScript の設定方法を説明していきます。まだ Composition API に慣れていない人もいると思うので Composition API の説明も各所で入れています。

Reactive での設定

<template>
  <div class="app">
    {{ state.msg }}
  </div>
</template>

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

export default defineComponent({
  name: 'App',
  components: {},
  setup() {
    const state = reactive({
      msg: 'Hello TypeScript',
    });
    return {
      state,
    };
  },
});
</script>

setup 関数を追加してその中で reactive 関数を利用してリアクティブな変数を定義します。

ブラウザ上には Hello TypeScript が表示されます。リアクティブな変数 msg を定義した時に型を設定していますせんが型推論によって string が設定されていることが確認できます。

reactive で data プロパティを設定

型を明示的に指定したい場合は as を利用して行います。

const state = reactive({
  msg: 'Hello TypeScript' as string,
});

string から number に変更するとエラーメッセージが表示されます。

reactive 関数を利用している場合は toRefs によって state を ref に変換することができます。ref に変換することで state.msg を msg に変更することができます。型への影響はありません。

<template>
  <div class="app">
    {{ msg }}
  </div>
</template>

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

export default defineComponent({
  name: 'App',
  components: {},
  setup() {
    const state = reactive({
      msg: 'Hello TypeScript' as string,
    });
    return {
      ...toRefs(state),
    };
  },
});
</script>

state には setup 関数の中でアクセスすることができます。msg の型は string なので state.msg に数値を設定するとエラーメッセージが表示されます。

setup() {
  const state = reactive({
    msg: 'Hello TypeScript' as string,
  });

  state.msg = 25;
state.msg に数値を設定

reactive 関数はジェネリックを使って型を設定することができます。

const state =
  reactive <
  { msg: string } >
  {
    msg: 'Hello TypeScript',
  };

さらに interface を利用して型を別に定義してその型を reactive 関数の型に利用することができます。

interface State {
  msg: string;
}
//略
const state =
  reactive <
  State >
  {
    msg: 'Hello TypeScript',
  };

ref での設定

reactive 関数から ref 関数に変更を行いリアクティブな変数 msg の設定を行います。

<template>
  <div class="app">
    {{ msg }}
  </div>
</template>

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

export default defineComponent({
  name: 'App',
  components: {},
  setup() {
    const msg = ref('Hello TypeScript');
    return {
      msg,
    };
  },
});
</script>

上記のように設定を行うと型推論により string が設定されます。


ref の場合に型を設定する場合、ジェネリックで型を設定することができます。

const msg = ref < string > 'Hello TypeScript';

ref で定義したデータにアクセスする場合は.value をつける必要があります。

msg.value = 'Hello Vue 3';

ref でオブジェクトの設定

オブジェクトの型を設定する場合は interface を利用します。interface の設定については Options API の時と同じです。

interface Book {
  title: string;
  author: string;
  year: number;
}

ref で型の Book で作成する場合はジェネリックで設定します。interface で設定した型で初期値を設定することができます。

const book =
  ref <
  Book >
  {
    title: 'Vue 3 Guide',
    author: 'Vue Team',
    year: 2020,
  };

異なる型を設定した場合にはエラーメッセージが表示されます。

型とは異なる値を設定した場合

外部リソースから book 情報を取得する場合には配列で保存します。配列の場合は以下のように型を設定することができます。

const books = ref<Book[]>([
  {
    title: 'Vue 3 Guide',
    author: 'Vue Team',
    year: 2020,
  },
  {
    title: 'Vite Guide',
    author: 'Vue Team',
    year: 2021,
  },
]);

props の場合

props は Composition API でも Options API の場合でも利用方法が同じなので型の設定も同じです。HelloWorld コンポーネントを使って確認します。

HelloWorld.vue ファイルに以下を記述します。props に型を設定する場合は PropType を import してます。

<template>
  <h1>{{ msg }}</h1>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';

export default defineComponent({
  name: 'HelloWorld',
  props: {
    msg: {
      type: String as PropType<string>,
    },
  },
});
</script>

ref で定義した msg プロパティを props として HelloWorld コンポーネントに渡しています。

<template>
  <div class="app">
    <hello-world :msg="msg" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import HelloWorld from './components/HelloWorld.vue';

export default defineComponent({
  name: 'App',
  components: {
    HelloWorld,
  },
  setup() {
    const msg = ref('Hello TypeScript');

    return {
      msg,
    };
  },
});
</script>

メソッド(関数)の場合

Options API では methods の中に関数を記述していましたが Composition API では setup 関数の中に関数を作成することができます。msg の値を変更すると changeMsg 関数を追加する。msg.value で値を変更することが可能です。追加した関数を template タグの中で利用するためには return のオブジェクトの中に changeMsg を含める必要があります。

setup() {
  const msg = ref('Hello TypeScript');

  const changeMsg = () => {
    msg.value = 'Hello Vue';
  };

  return {
    msg,
    changeMsg,
  };
},

型推論によって msg の型が string に設定されているので上記のコードでは問題はありませんが msg.value の値を数値に変更するとエラーメッセージが表示されます。

数値を変更するとエラー

Composition API の場合も reactive, ref, props, 関数での TypeScript の設定方法の基礎を確認することができました。