Vueを含めJavaScriptを使った開発で型定義を使いたい場合にTypeScriptの知識が必要になります。本文書ではTypeScriptをこれからマスターしていきたいという人向けにVueのComposition APIを利用した場合の型の設定について説明を行っています。

Composition APIとscript setupを利用することでVue環境でのTypeScriptの設定・利用が簡単になります。そのため本文書でもComposition APIとscript setupを利用して説明を行っていきます。Options APIにおけるTypeScriptの設定について下記の文書で公開済みです。

エディターはVisual Studio Code(VSCode)を利用して拡張機能でVolarをインストールしてWindowsで動作確認を行っています。

TypeScript環境の構築

Vue3はTypeScriptで記述されていることからVue3でTypeScriptを利用したい場合、公式のプロジェクト作成ツールを利用することで TypeScriptの開発環境を簡単に構築することができます。

現在Vueプロジェクトを作成する際に推奨されている方法ではnpm init vue@latestコマンドを実行してプロジェクトを作成します。コマンドを実行すると内部でcreate-vueが実行され、TypeScriptを含めVueプロジェクトで利用頻度の高い機能を選択してインストールすることができます。

npm init vue@latestコマンド実行後に選択できる機能については下記の文書で公開しています。

npm init vue@latestコマンドを実行してVueプロジェクトの作成を行います。実行するとプロジェクト名を聞かれるのでプロジェクト名を入力してください。任意の名前をつけることができるのでここではvue3-typescriptという名前をつけています。本文書ではTypeScriptのみ”Yes”を選択して進めています。


 % npm init vue@latest
Need to install the following packages:
  create-vue@latest
Ok to proceed? (y) y

Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue3-typescript
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /Users/mac/Desktop/vue3-typescript...

Done. Now run:

  cd vue3-typescript
  npm install
  npm run dev

コマンド実行後プロジェクト名のフォルダが作成されるのでフォルダに移動してnpm installを実行してパッケージのインストールを行います。


 % cd vue-typescript
 % npm install

npm installの実行完了後、npm run devコマンドを実行すると開発サーバが起動します。Vueプロジェクトの作成は完了です。

これからTypeScriptの動作確認を行っていきます。

Reactiveなデータの設定

Options APIではデータプロパティを定義することでReactiveなデータを定義することができました。Composition APIでReactiveなデータを定義するためにreactive関数、ref関数のどちらかを利用します。

Reactiveとは

TypeScriptの前にComposition APIでのReactiveなデータの理解とどのように扱うのか確認しておきましょう。

App.vueファイルでcount変数を定義してclickイベントでcountの数が1ずつ増える設定を行います。


<script setup lang="ts">
const count = 0;
</script>

<template>
  <div>Count:{{ count }}</div>
  <button @click="count++">Add Count</button>
</template>

count変数を定義していますがReactiveなデータではないためcountの値は表示されてもボタンを押してもcountの数が増えることはありません。

count変数を利用してcout数が増えるか確認
count変数を利用してcout数が増えるか確認

ref関数を利用してReactiveな変数countを定義します。ref関数の引数にはcountの初期値の0を設定しています。


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

const count = ref(0);
</script>

<template>
  <div>Count:{{ count }}</div>
  <button @click="count++">Add Count</button>
</template>

countはReactiveな変数なのでボタンをクリックするとcountの数が増えます。これがReactiveなデータとReactiveでないデータの違いです。

ref関数を利用してreactiveな変数を作成
ref関数を利用してreactiveな変数を作成

ref関数の型設定

TypeScriptの環境でReactiveなデータとは何かを説明しましたが型についての設定は全く行っていません。それはTypeScriptにより型推論(Type inference)により自動的に型の設定が行われるため型の設定を省略できるためです。

型推論によってcount変数にどのような型が設定されているのかはVSCodeを利用して拡張機能にVolarをインストールしている場合はcountにカーソルを当てると型が表示されます。

エディターで型を確認
エディターで型を確認

countにはRef<number>が設定されていることがわかります。型推論によって型が自動で設定されているためclickイベントでcountに文字列を入力しようとするとメッセージで型が異なっていることを教えてくれます。

異なる型を代入しようとした場合のエラー
異なる型を代入しようとした場合のエラー

明示的に型を設定したい場合にはジェネリクスを利用して設定することができます。


const count= ref<number>(0);

このほかにvueからRef型をimportして下記のように設定することもできます。


<script setup lang="ts">
import { ref } from 'vue';
import type { Ref } from 'vue';

const count: Ref<number> = ref(0);
</script>

reactive関数の型設定

reactive関数の場合はどのように型を設定するのか確認していきます。reactive関数でreactiveな変数userを定義してFullNameとしてブラウザ上に名前を表示させています。


<script setup lang="ts">
import { reactive } from 'vue';
const user = reactive({
  firstName: 'John',
  lastName: 'Doe',
  age: 25,
});
</script>

<template>
  <div>FullName: {{ `${user.firstName} ${user.lastName}` }}</div>
</template>

reactive関数を設定時に型を設定していませんが型推論により型を確認することができます。

型推論によるuserの型の確認
型推論によるuserの型の確認

明示的に型を設定したい場合は下記のように定義した変数userの右側に:(コロン)を付けて設定することができます。オブジェクトの記述方法と似ていますがプロパティと型のペアを複数設定する場合に,(コンマ)を利用していないので注意してください。型を指定することができましたがオブジェクトのプロパティの数が増えてくるとプロパティ名や型の設定の記述間違いを起こしやすくコードも読みにくくなります。


const user: {
  firstName: string;
  lastName: string;
  age: number;
} = reactive({
  firstName: 'John',
  lastName: 'Doe',
  age: 25,
});

interfaceを利用することで型を別に定義することができるためコードがすっきりし読みやすくなります。さらにinterfaceはimport, exportすることができるので一度どこかで定義することで型の再利用を行うことができます。定義の場所を1つにすることで毎回型を記述することによる間違いをなくすことができます。


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

const user: User = reactive({
  firstName: 'John',
  lastName: 'Doe',
  age: 25,
});

オブジェクトの型を設定できるのはinterfaceだけではなくtype(型エイリアス)を利用することができます。interfaceとtypeは記述方法は似ていますがtypeには=(イコール)を利用して設定するなど違いがあるので注意が必要です。


type User = {
  firstName: string;
  lastName: string;
  age: number;
};

const user: User = reactive({
  firstName: 'John',
  lastName: 'Doe',
  age: 25,
});

その他のinterface、typeとTypeScriptの基本的な機能については下記の文書で公開しています。

型アサーション

userを定義する際に初期値が空のオブジェクトで後ほどコードの中でプロパティを設定するという場合もあるかもしれません。


const user = reactive({})

user.firstName="John",
user.lastName="Doe",
user.age = 25;

型推論によりuserの型は{}になっているのでプロパティに値を設定しようとするとエラーメッセージが表示されます。

空のオブジェクトが設定される
空のオブジェクトが設定される

その場合に型アサーションを利用することで型の上書きを行うことができます。


const user = reactive({} as User)

user.firstName="John",
user.lastName="Doe",
user.age = 25;

computedプロパティの型設定

computedプロパティにおける型の設定について確認を行っていきます。computedプロパティは定義済みの変数を利用して計算、加工を行うことで元の変数のデータとは異なる形でユーザに表示することができる機能です。reactiveな変数を利用してcomputedプロパティを定義した場合はreactiveな変数が更新されるとその更新に合わせて再計算、再加工が自動で行われます。

Composition APIでcomputedプロパティを利用するためにはvueからcomputed関数をimportする必要があります。computedプロパティではcomputed関数の引数に関数を設定します。動作確認のためにcomputedプロパティを設定しますがここで設定するcomputedプロパティはreactive関数で定義したuser変数を利用してfirstNameとlastNameを結合して表示するだけのシンプルなものです。


import { reactive, computed } from 'vue';

type User = {
  firstName: string;
  lastName: string;
  age: number;
};

const user: User = reactive({
  firstName: 'John',
  lastName: 'Doe',
  age: 25,
});

const fullName = computed(() =>`${user.firstName} ${user.lastName}`);

fullNameにカーソルを当てると型推論によりfullNameの型が自動で設定されComputedRef<string>であることがわかります。

computedプロパティの型の確認
computedプロパティの型の確認

明示的に型を指定したい場合はrefのようにジェネリクスを利用して設定を行うことができます。Computedプロパティの引数で設定した関数の戻り値の型を設定します。


const fullName = computed<string>(() => `${user.firstName} ${user.lastName}`);

関数の型設定

VueのOptions APIではデータプロパティを更新する際にはmethods(メソッド)に関数を追加していましたがCompotions APIではscriptタグの中にそのまま関数を記述することがreactiveな変数を更新することができます。TypeScriptにおける関数の型の設定と方法は同じです。

changeNameを引数なしで定義してuser.firstNameの値を”Jane”に設定し実行します。


const changeName = () => {
  user.firstName = "Jane";
};

changeName();

ブラウザ上には”Jane Doe”と表示され関数の処理は正常に動作します。

関数の場合は引数と戻り値に対して型の設定を行うことができます。戻り値の設定を行っていない場合でも型推論によって自動で型が設定されます。

関数の型推論を確認
関数の型推論を確認

changeName関数には戻り値がないため戻り値の型がvoid型になっていることがわかります。戻り値の型を明示的に指定したい場合には下記のように設定することができます。


const changeName = ():void => {
  user.firstName = "Jane";
};

次に関数に引数を設定します。


const changeName = (name):void => {
  user.firstName = name;
};

changeName('Jane');

引数でnameを指定していますが型の指定がないのでVSCode上ではnameの下に波線が表示されます。メッセージが表示されます。nameにカーソルを合わせるとメッセージには”nameは暗黙的にanyタイプを持っている”と表示されます。

引数に型を指定しなかった場合
引数に型を指定しなかった場合

npm run devコマンド上にはエラーのメッセージは発生していませんがTypeScriptの型チェックを行うことができるnpm run typeckeckコマンドを実行してみましょう。npm run typecheckコマンドが実行できることはpackage.jsonファイルのscriptsにtypecheckが登録されていることで確認することができます。実行するとVSCode上に表示されていたエラーと同じ内容が表示されます。


 % npm run typecheck

> vue3-typescript@0.0.0 typecheck
> vue-tsc --noEmit

src/App.vue:18:21 - error TS7006: Parameter 'name' implicitly has an 'any' type.

18 const changeName = (name): void => {

メッセージを見るとわかるように暗黙的にany型が設定されていることが原因でエラーになっています。暗黙的にany型が設定されていると必ずエラーになるわけではなくTypeSciprtのコンパイラオプションのnoImplicitAnyで制御することができます。noImplicitAnyという名前からimplicit(暗黙的)なany型は許可しないということを表しています。これはtsconfig.jsファイルでstrictがtrueになっているためnoImplicitAnyが有効になっています。tsconfig.jsファイルでstrictの値をfalseに設定すると暗黙的なanyが許可されることになりエラーは消えます。


{
  "extends": "@vue/tsconfig/tsconfig.web.json",
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
  "compilerOptions": {
    "strict": false,//試しに設定
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  //略

strictの値をtrueに設定することでnoImplicitAnyを含め7つのオプションが有効になります。strictという名前の通り設定を行うと型のチェックが厳格になります。

strictをfalseに変更した後にnpm run typecheckコマンドを実行するとエラーメッセージは消えます。

tsconfig.jsonの設定値についてはextendsに設定されている@vue/tsconfig/tsconfig.web.jsonを見ると@vue/tsconfig/のtsconfig.jsonに設定されていることがわかります。

strictをtsconfig.jsonでtrueに設定している場合は関数の引数には必ず型を指定しないといけないことがわかります。

tsconfig.jsonファイルを更新すると設定値が反映されることが確認できたのでstrictはtrueに戻しておいてください。any型を利用するとどのような値でも入れることができるためコードの品質が下がってしまうのでstrictはtrueに設定しておきます。

関数の戻り値については型推論で型が設定されているため明示的に設定する必要はありませんでしたが引数の場合は明示的に型を設定する必要があります。


const changeName = (name:string): void => {
  user.firstName = name;
};

引数、戻り値の型を設定しましたが関数についても型を設定することができます。アロー関数と非常によく似ていますが下記のように型を設定することができます。(name:string)=>voidの部分が関数に設定した型を表しています。


const changeName: (name: string) => void = (name: string): void => {
  user.firstName = name;
};

Propsの型設定

propsは親子関係を持つコンポーネントでデータの受け渡しを行うために利用する機能です。動作確認するためには別のコンポーネントを作成する必要があります。デフォルトから存在するsrc¥componentsフォルダにあるHelloWorld.vueファイルを利用します。

HelloWorld.vueではpropsの設定が行われているので設定されているpropsを利用して以下のように記述します。


<script setup lang="ts">
defineProps<{
  msg: string;
}>();
</script>

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

App.vueファイルで作成したHelloWorld.vueファイルをimportしてpropsでmsgを渡します。


<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
</script>

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

ブラウザ上にはHelloが表示されます。

”script setup”とComposition APIでpropsを利用する場合にはdefineProps関数を利用します。refやcomputedと異なりdefineProps関数をimportする必要はありません。defineProps関数はコンパイラーマクロと呼ばれscript setupの中で利用することができます。その他のコンパイラーマクロには後ほど説明するemitで利用されるdefineEmit関数があります。

上記のHelloWorldコンポーネントの設定で確認したようにpropsで渡される変数は<>の中で型を設定することができます。

複数のpropsがある場合の設定方法を確認します。ref関数を利用して定義したuserをpropsで渡します。


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

export type User = {
  firstName: string;
  lastName: string;
  age: number;
};

const user = ref<User>({
  firstName: "John",
  lastName: "Doe",
  age: 25,
});
</script>

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

type(型エイリアス)やinterfaceなどで設定した型はexportして別のコンポーネントで利用することができます。User型もHelloWorldコンポーネントで利用できるようにexportをつけています。exportを利用して型を再利用できることでオブジェクトのような記述量の多い型を毎回すべて記述する必要がなくなります。

HelloWorld.vueファイルではdefinePropsを使ってpropsのmsgとuserの型を設定します。User型はApp.vueファイルからimportしています。


<script setup lang="ts">
import type { User } from "../App.vue";
defineProps<{
  msg: string;
  user: User;
}>();
</script>

<template>
  <div>
    <h1>{{ msg }}</h1>
    <div>fullName: {{ `${user.firstName} ${user.lastName}` }}</div>
  </div>
</template>

複数のpropsがある場合は上記のように設定を行うことができます。Propsという名前のinterfaceにまとめています。


<script setup lang="ts">
import type { User } from "../App.vue";
interface Props {
  msg: string;
  user: User;
}
defineProps<Props>();
</script>

<template>
  <div>
    <h1>{{ msg }}</h1>
    <div>fullName: {{ `${user.firstName} ${user.lastName}` }}</div>
  </div>
</template>

Emitの型設定

親コンポーネントから子コンポーネントに値を渡したい場合にpropsを利用することができます。その逆で子コンポーネントから親コンポーネントに対して通知または値を渡したい場合に利用できるのがemitです。

HelloWorld.vueファイルにボタンを追加してclickイベントを利用してchangeName関数を設定します。ボタンをクリックするとchangeName関数が実行されます。propsを通してuserオブジェクトが渡されていますがpropsで渡されたデータを子コンポーネントで更新することはできません。


<script setup lang="ts">
import type { User } from "../App.vue";
interface Props {
  msg: string;
  user: User;
}
defineProps<Props>();

const changeName = () => {
  console.log("change name");
};
</script>

<template>
  <div>
    <h1>{{ msg }}</h1>
    <div>fullName: {{ `${user.firstName} ${user.lastName}` }}</div>
    <button @click="changeName">Change Name</button>
  </div>

userオブジェクトに含まれるプロパティを更新したい場合は親コンポーネントであるApp.vueファイルで行う必要があります。emitを利用して親コンポーネントに対して変更したい値を渡すことができます。

emitを利用するためにはdefineEmits関数を利用してemitの設定を行う必要があります。defineEmitsの引数には関数の型の定義(コールシグネチャー)を記述します。eventはemitを実行する時に設定するイベント名を設定します。このイベント名を利用して親コンポーネントで子コンポーネントから送られてくるイベントを識別することができます。第2引数には親コンポーネントに渡す変数(payload:ペイロード)の型を設定します。関数の戻り値はないのでvoid型を設定しています。渡す値がない場合は第2引数は省略することができます。


const emit = defineEmits<{
  (event: "changeName", firstName: string): void;
}>();

emitで設定した定義を元にchangeName関数で実行するemitの処理を記述します。イベント名にはdefineEmitsで設定したchangeNameを設定し、親コンポーネントには文字列(string)のJaneを渡します。これで子コンポーネント側のemitの設定は完了です。


const changeName = () => {
  emit("changeName", "Jane");
};

子コンポーネントの設定が完了したら親コンポーネントでchangeNameイベントを検知する設定を行います。


<template>
  <HelloWorld msg="Hello" :user="user" @changeName="changeName" />
</template>

changeNameイベントの検知は@changeNameで行うことができます。changeNameイベントを検知するとchangeName関数を実行します。イベント名と関数名を同じにしていますが関数名を変更することは可能です。changeNameの引数にはemitで渡した値が入るため型を同じstringに設定します。


const changeName = (firstName: string) => {
  user.value.firstName = firstName;
};

ブラウザで動作確認を行うと”Change Name”ボタンをクリックするとブラウザ上に表示されている”John Doe”が”Jane Doe”に変わります。

emitを利用してfirstNameを変更
emitを利用してfirstNameを変更

reactiveな値を渡す

親コンポーネントに渡していた値が”Jane”に設定していたので固定値ではなく入力した値を渡せるようにinput要素を追加します。input要素に入力した値を取得できるようにv-modelディレクティブでfirstNameを指定します。firstNameはreactiveな変数でref関数を利用して定義します。


<script setup lang="ts">
import { ref } from "vue";
import type { User } from "../App.vue";
interface Props {
  msg: string;
  user: User;
}
defineProps<Props>();

const emit = defineEmits<{
  (event: "changeName", firstName: string): void;
}>();

const changeName = () => {
  emit("changeName", firstName.value);
};

const firstName = ref<string>("");
</script>

<template>
  <div>
    <h1>{{ msg }}</h1>
    <div>fullName: {{ `${user.firstName} ${user.lastName}` }}</div>
    <input v-model="firstName" /><br />
    <button @click="changeName">Change Name</button>
  </div>

firstNameの初期値は””として型はstringに設定しています。親コンポーネントに渡す値はref関数なのでfirstName.valueの形で渡しています。

input要素に任意の名前を入力して”Change Name”ボタンをクリックすると名前が更新されることが確認できます。

ref関数で定義したreactiveな値を使って更新
ref関数で定義したreactiveな値を使って更新

ref関数で定義したfirstNameを渡す時にfirstName.valueで渡していましたがfirstNameで渡せるか確認します。

emit関数の第2引数をfirstNameに変更すると型がRef<string>に変わるためメッセージが表示されます。メッセージが表示されるのはdefineEmitsの中で設定したfirstNameの型がstringのため型が一致しないためです。

emitの型が異なるためメッセージ
emitの型が異なるためメッセージ

firstNameの型をstringからRef<string>に変更します。Refを利用する場合はRefをvueからimportする必要があります。


import { ref } from "vue";
import type { Ref } from "vue";
//略

const emit = defineEmits<{
  (event: "changeName", firstName: Ref<string>): void;
}>();

HelloWorld.vueファイルの更新を行なったのでApp.vueでメッセージが表示されます。defineEmitsで関数の型を変更したのでApp.vue側のchangeNameの引数の型を一致させる必要があります。

changeNameイベントの型に関するメッセージ
changeNameイベントの型に関するメッセージ

firstNameの型をstringからRef<string>に変更します。Refのimportも必要になります。値はfistName.valueで取得することができます。


import HelloWorld from "./components/HelloWorld.vue";
import { ref } from "vue";
import type { Ref } from "vue";
//略
const changeName = (firstName: Ref<string>) => {
  user.value.firstName = firstName.value;
};

型を正しく設定することでemitでreactiveな変数を送る際には2つの方法があることがわかりました。

emitの別の記述方法

defineEmitsの引数では関数の型を設定していましたがイベント名のみ設定することも可能です。


const emit = defineEmits(["changeName"]);

イベント名が一致するので上記の設定でemitを実行することができますが型の定義を行っていないため引数に数値にしてもVScode上でメッセージで教えてくれるということはありません。

Template Refsの型設定

Vue.jsではDOM要素に直接アクセスを行いたい場合にもref関数を利用することができます。Template Refsでref関数を利用した場合の型の設定を確認します。

App.vueファイルでrefを利用してinput要素にアクセスできるように以下を記述します。アクセスしたい要素にref属性を設定して任意の名前を設定します。ref関数を利用して定義する変数は要素のref属性で指定した名前と同じ名前を設定する必要があります。


<script setup lang="ts">
import { ref,onMounted } from "vue";

const input = ref(null);

onMounted(() => {
  input.value.focus();
});
</script>

<template>
  <input ref="input" />
</template>

上記のコードはJavaScriptでは問題ありませんがTypeScriptで利用するとinput.valueの下に波線が表示されメッセージが表示されます。メッセージの内容は”オブジェクトは ‘null’ である可能性があります。ts(2531)”です。原因は初期値にnullを設定しているためです。型を明示せずにメッセージを解消するためにref関数の初期値をnullから空白に変更します。


const input = ref();

onMounted(() => {
  input.value.focus();
});

input変数にカーソルを当てると型を確認することができます。Ref<any>となっておりジェネリクスにはany型が設定されています。any型はどのような値でも利用することができるためメッセージが解消されます。

型の確認
型の確認

変数inputの中にinput要素の情報が含まれているのか確認するためにinput.valueの中身をconsole.logに指定します。


const input = ref();

onMounted(() => {
  console.log(input.value);
});

ブラウザのデベロッパーツールのコンソールには<input />が表示され変数inputの中にinput要素が含まれていることが確認できます。

input要素はfocusメソッド、valueプロパティを持っているので下記のように設定することでブラウザで表示した時にその要素にフォーカスが当たりvalueの値をデベロッパーツールのコンソールに表示することができます。


<script setup lang="ts">
import { ref, onMounted } from "vue";

const name = ref<string>("John");
const input = ref();

onMounted(() => {
  input.value.focus();
  console.log(input.value.value);
});
</script>

<template>
  <input ref="input" v-model="name" />
</template>

ブラウザのデベロッパーツールのコンソールには”John”が表示されます。any型が設定されているので特に型に関するメッセージが表示されることなく動作することがわかりました。

TypeScriptではDOM要素に型を設定することができるのでany型ではなく明示的に型を設定してみましょう。input要素の型はHTMLinputElementなのでHTMIInputElementを設定します。


const input = ref<HTMLInputElement>();

HTMLInputElementを設定するとinput変数の型は型推論によってRef<HTMLInputElement | undefined>になりinputの値がundifinedの場合はinput.valueは値を持たないので下記のメッセージが表示されます。

HTMLInputElementの型を設定するとメッセージ
HTMLInputElementの型を設定するとメッセージ

input.valueが値を持つかどうかのチェックをいれることでメッセージは解消されます。


onMounted(() => {
  if (input.value) {
    input.value.focus();
    console.log(input.value.value);
  }
});

if文による分岐ではなくoptional chainningを利用することでもメッセージは解消されます。


onMounted(() => {
    input.value?.focus();
    console.log(input.value?.value);
});

union型を利用してundefinedも明示的に設定することもできます。


const input = ref<HTMLInputElement | undefined>();

const input = ref<HTMLInputElement | undefined>();

ref関数の値が空白ではなく最初に設定していた初期値のnullにした場合も下記のように記述することができます。


const input = ref<HTMLInputElement | null>(null);

HTMLInputElementの型の確認

input要素の型に突然HTMLInputElementが出てきましたがHTMLInputElementの型を具体的に確認するのはネット上で検索することもできますがVSCodeの場合HTMLInputElementにカーソルを合わせて”Ctrl + クリック”すると型の詳細を確認することができます。

HTMLInputElementはHTMLElementをexteds(継承)していることもわかります。

HTMLIputElementの型の確認
HTMLIputElementの型の確認

スクロールしていくとvalueプロパティを見つけることができstring型が設定されていることがわかります。

inputの型をHTMLElementに変更するとHTMLElementの型はvalueプロパティを持っていないのでメッセージが表示されます。


const input = ref<HTMLElement | null>(null);
HTMLElementにはvalueプロパティはない
HTMLElementにはvalueプロパティはない

HTMLElementはHTMLOrSVGElementをexteds(継承)しているのでfocusメソッドは持っているためinput.valueの行を削除するとメッセージは消えます。

focusメソッドの型は存在
focusメソッドの型は存在

template refsを利用して直接要素にアクセスする場合、明示的に型を設定する場合はアクセスする要素がどの要素なのかを確認して正しい型を設定する必要があります。

例えばaタグであればHTMLAnchorElementの型を設定することでhrefへのアクセスを行ってもメッセージが表示されることはありません。HTMLElementやHTMLInputElementの型を設定するとプロパティhrefはHTMLElementに存在しませんと表示されます。


<script setup lang="ts">
import { ref, onMounted } from "vue";

const name = ref<string>("John");
const input = ref<HTMLElement | null>(null);
const ahref = ref<HTMLAnchorElement | null>(null);

onMounted(() => {
  input.value?.focus();
  console.log(ahref.value?.href);
});
</script>

<template>
  <input ref="input" v-model="name" />
  <a ref="ahref" href="https://google.com">Google</a>
</template>

template refsでの型設定を通してHTMLElementの型の基礎も理解できるようになりました。

Eventの型設定

Eventに関する型設定について確認していきます。input要素を準備してchangeイベントを設定します。input要素に入力を行いカーソルを外すとhandleChange関数が実行されます。


<script setup lang="ts">
const handleChange = (event) => {
  console.log(event.target.value);
};
</script>

<template>
  <input @change="handleChange" />
</template>

上記のコードの場合は引数に型を設定してないのでメッセージが表示されます。関数の時に確認しましたが引数に型を設定していない場合はany型が設定されますがTypeScriptの設定ファイルのtsconfig.jsonでstrictをtrueに設定しているため”noImplicitAny”が有効になり暗黙的なanyは利用することはできません。

引数に型を設定していない場合
引数に型を設定していない場合

eventにはEvent型があるので設定します。Event型を指定すると今後はevent.target.valueの下に波線が表示されます。メッセージを確認するとオブジェクトがnullの可能性があるということです。

eventオブジェクトはnullの可能性
eventオブジェクトはnullの可能性

eventのtargetに何が入っているのか確認してみましょう。


const handleChange = (event: Event) => {
  console.log(event.target);
};

ブラウザのデベロッパーツールのコンソールには<input />が入っていることがわかります。input要素の型はtemplate Refsの中で確認した通りHTMLInputElementであったことを思い出しましょう。

changeイベントを実行する要素は必ずinput要素であることがわかっているのでevent.targetには必ずinput要素がはいります。nullを避けるためにevent.targetの型を型アサーションを使ってHTMLInputElementの型を上書きします。


<script setup lang="ts">
const handleChange = (event: Event) => {
  console.log((event.target as HTMLInputElement).value);
};
</script>

<template>
  <input @change="handleChange" />
</template>

メッセージは消えてinput要素に文字列を入力してカーソルを外すと入力した文字列がデベロッパーツールのコンソールに表示されます。

引数があった場合

handleChange関数に引数があった場合も確認しておきます。以下のようにhandleChangeに$eventを利用することで引数とは別にイベントを取得することができます。


<script setup lang="ts">
const handleChange = (event: Event, comment: string) => {
  console.log((event.target as HTMLInputElement).value);
    //const target = event.target as HTMLInputElement
  //console.log(target.value)
  console.log(comment);
};
</script>

<template>
  <input @change="handleChange($event, 'eventの型の確認')" />
</template>

mousemoveイベントの例

mousemoveイベントを使ってマウスの位置情報を取得するコードを利用してEventの型について確認を行います。


<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const x = ref<number>(0);
const y = ref<number>(0);

const handleMouseMove = (event: Event) => {
  x.value = event.pageX;
  y.value = event.pageY;
};

onMounted(() => {
  window.addEventListener("mousemove", handleMouseMove);
});

onUnmounted(() => {
  window.removeEventListener("mousemove", handleMouseMove);
});
</script>

<template>
  <div>
    <span>x:{{ x }} y:{{ y }}</span>
  </div>
</template>

eventにEvent型を設定しているのでこれで問題ないように思うかもしれませんがpageXとpageYに波線が表示されます。メッセージにはEvent型はプロパティのXを持っていないといった内容です。VScodeであればEventにカーソルを当て”Ctrl + クリック”をしてEventの中身を確認します。プロパティのtargetやtypeなどを見つけることができますがpageX, pageYはありません。

Event型の内容を確認
Event型の内容を確認

HTMLElementを継承していたHTMLInputElementやHTMLDivElementがあるようにEventを継承するMouseEvent型が存在します。MouseEvent型を設定するとメッセージの表示は消えます。


const handleMouseMove = (event: MouseEvent) => {
  x.value = event.pageX;
  y.value = event.pageY;
};
</script>

MouseEvent型の中身を見るとpageX、pageYを確認することができます。UIEventをextends(継承)していますがUIEventはさらにEventをextendsしています。

MouseEventの中身を確認
MouseEventの中身を確認

Eventといってもさまざまな型が存在するのでイベントに合わせた型を選択する必要があります。

fetch関数の型設定

外部のリソースからfetch関数を利用して取得したデータをv-forディレクティブを利用して展開表示する場合の設定を確認しておきます。外部リソースにはJSONPlaceholerを利用します。無料のサービスでhttps://jsonplaceholder.typicode.com/usersにアクセスすると10名分のユーザ情報を取得することができます。

TypeScriptを利用しない場合は下記のように記述することでブラウザ上に10名分のユーザ名を表示することができます。取得する際にはfetch関数とasync, await関数を利用しています。


<script setup lang="ts">
import { ref } from "vue";
const users = ref([]);

const fetchUsers = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  users.value = await res.json();
};

fetchUsers();
</script>

<template>
  <div v-for="user in users" :key="user.id">
    {{ user.name }}
  </div>
</template>

このコードをそのままTypeScriptで利用しようとするとv-forディレクティブのkeyのuser.idで”プロパティ ‘id’ は型 ‘never’ に存在しません。”と表示されます。user.nameも同様のメッセージが表示されます。

変数usersにカーソルを合わせるとRef<never[]>で配列のnever型が設定されていることができます。

型推論でnever型
型推論でnever型

ref関数に明示的に型を設定します。JSONPlaceHolderから戻されるユーザ情報を確認する必要があります。直接ブラウザのURLにhttps://jsonplaceholder.typicode.com/usersを入力することでユーザ情報を取得することができます。


[
  {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
      "geo": {
        "lat": "-37.3159",
        "lng": "81.1496"
      }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
      "name": "Romaguera-Crona",
      "catchPhrase": "Multi-layered client-server neural-net",
      "bs": "harness real-time e-markets"
    }
  },
  {
    "id": 2,
    "name": "Ervin Howell",
    "username": "A
//略

この情報を元にinterfaceでUserを定義します。戻されるユーザ情報には複数のプロパティがありますがここではコードが長くなるので一部のみ設定を行います。


interface User {
  id: number;
  name: string;
  email: string;
  address: {
    street: string;
  };
}

interfaceのUserを作成後ref関数に型を設定します。本文書で説明済みですがref関数は2つの方法で型を設定することができます。


const users = ref<User[]>([]);
 or
const users: Ref<User[]> = ref([]);
※Refはimport type {Ref} from 'vue'が必要

ref関数に型を設定するとuser.idのメッセージが解消されブラウザ上には取得したユーザ一覧が表示されます。

ユーザ一覧の表示
ユーザ一覧の表示

ユーザ一覧をブラウザ上に表示することができましたがTypeScriptをさらに理解できるようにfetchUsers関数の中での型を確認していきましょう。

fetchUsersにカーソルを当てると型推論により関数の戻り値がPromise<void>であることがわかります。asyncの戻り値はPromiseのジェネリクスに戻り値の型を入れることがわかります。

fetchUsers関数の戻り値に確認
fetchUsers関数の戻り値に確認

明示的に型を指定したい場合は下記のように行うことができます。戻り値がないのでvoid型を指定しています。


const fetchUsers = async (): Promise<void> => {
  const res = await fetch(
    "https://jsonplaceholder.typicode.com/users"
  );
  users.value = await res.json();
};

もしusers.valueのように値を戻す場合はvoid型ではなく戻される型(User[])を設定することになります。


const fetchUsers = async (): Promise<User[]> => {
  const res = await fetch(
    "https://jsonplaceholder.typicode.com/users"
  );
  users.value = await res.json();
  return users.value;
};

次はawaitで実行されるfecth関数の戻り値を確認するために変数のresにカーソルを当てます。

型推論によりResponse型が設定されていることがわかります。Responseの型とはどのようなものなのでしょう。

await fetchの戻り値を確認
await fetchの戻り値を確認

明示的に戻り値にResponse型を設定して型の中身を確認してみましょう。resにResponse型を設定した後にVSCodeの場合はResponseにカーソルを当てて”Ctrl + クリック”で型情報が表示されます。


const res:Response= await fetch("https://jsonplaceholder.typicode.com/users");

interface Responseの型は下記のように設定されています。Responseはbodyをextends(継承)しているのでBody型の中身も一緒に確認してみましょう。


interface Response extends Body {
    readonly headers: Headers;
    readonly ok: boolean;
    readonly redirected: boolean;
    readonly status: number;
    readonly statusText: string;
    readonly type: ResponseType;
    readonly url: string;
    clone(): Response;
}
//Boty
interface Body {
    readonly body: ReadableStream<Uint8Array> | null;
    readonly bodyUsed: boolean;
    arrayBuffer(): Promise<ArrayBuffer>;
    blob(): Promise<Blob>;
    formData(): Promise<FormData>;
    json(): Promise<any>;
    text(): Promise<string>;
}

interfaceを構成するプロパティの中にはstatusやokなど見慣れたものもあるのではないでしょうか。見慣れていない人もresの中身をconsole.logで出力することでReponseのプロパティと変数resに含まれるプロパティの情報が一致することを理解することができます。


const fetchUsers = async (): Promise<User[]> => {
  const res: Response = await fetch(
    "https://jsonplaceholder.typicode.com/users"
  );
  console.log(res);
  users.value = await res.json();
  return users.value;
};

ブラウザのデベロッパーツールのコンソールを確認します。interfaceのReponseで定義されているプロパティとresに含まれるプロパティが一致することがわかります。

変数resの中身を確認
変数resの中身を確認

実際の値と型を比較することでResponse型の理解も深まったかと思います。取得したresオブジェクトからユーザデータを取得するjsonメソッドはInterfaceのBody型に含まれているので戻り値がPromise<any>であることもわかります。


users.value = await res.json();

このようにResponseやHTMLElementなど自分で設定を行っていない型を確認していくことでその型を設定した変数がどのようなプロパティやメソッドを持っているのかを確認することができます。型を理解することができればどのような機能を持っているかも理解できるようになっていきます。