【Vue+TypeScript】Composition APIでTypeScript入門

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 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ずつ増える設定を行います。
count変数を定義していますがReactiveなデータではないためcountの値は表示されてもボタンを押してもcountの数が増えることはありません。

ref関数を利用してReactiveな変数countを定義します。ref関数の引数にはcountの初期値の0を設定しています。
countはReactiveな変数なのでボタンをクリックするとcountの数が増えます。これがReactiveなデータとReactiveでないデータの違いです。

ref関数の型設定
TypeScriptの環境でReactiveなデータとは何かを説明しましたが型についての設定は全く行っていません。それはTypeScriptにより型推論(Type inference)により自動的に型の設定が行われるため型の設定を省略できるためです。
型推論によってcount変数にどのような型が設定されているのかはVSCodeを利用して拡張機能にVolarをインストールしている場合はcountにカーソルを当てると型が表示されます。

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

明示的に型を設定したい場合にはジェネリクスを利用して設定することができます。
このほかにvueからRef型をimportして下記のように設定することもできます。
reactive関数の型設定
reactive関数の場合はどのように型を設定するのか確認していきます。reactive関数でreactiveな変数userを定義してFullNameとしてブラウザ上に名前を表示させています。
reactive関数を設定時に型を設定していませんが型推論により型を確認することができます。

明示的に型を設定したい場合は下記のように定義した変数userの右側に:(コロン)を付けて設定することができます。オブジェクトの記述方法と似ていますがプロパティと型のペアを複数設定する場合に,(コンマ)を利用していないので注意してください。型を指定することができましたがオブジェクトのプロパティの数が増えてくるとプロパティ名や型の設定の記述間違いを起こしやすくコードも読みにくくなります。
interfaceを利用することで型を別に定義することができるためコードがすっきりし読みやすくなります。さらにinterfaceはimport, exportすることができるので一度どこかで定義することで型の再利用を行うことができます。定義の場所を1つにすることで毎回型を記述することによる間違いをなくすことができます。
オブジェクトの型を設定できるのはinterfaceだけではなくtype(型エイリアス)を利用することができます。interfaceとtypeは記述方法は似ていますがtypeには=(イコール)を利用して設定するなど違いがあるので注意が必要です。
その他のinterface、typeとTypeScriptの基本的な機能については下記の文書で公開しています。
型アサーション
userを定義する際に初期値が空のオブジェクトで後ほどコードの中でプロパティを設定するという場合もあるかもしれません。
型推論によりuserの型は{}になっているのでプロパティに値を設定しようとするとエラーメッセージが表示されます。

その場合に型アサーションを利用することで型の上書きを行うことができます。
computedプロパティの型設定
computedプロパティにおける型の設定について確認を行っていきます。computedプロパティは定義済みの変数を利用して計算、加工を行うことで元の変数のデータとは異なる形でユーザに表示することができる機能です。reactiveな変数を利用してcomputedプロパティを定義した場合はreactiveな変数が更新されるとその更新に合わせて再計算、再加工が自動で行われます。
Composition APIでcomputedプロパティを利用するためにはvueからcomputed関数をimportする必要があります。computedプロパティではcomputed関数の引数に関数を設定します。動作確認のためにcomputedプロパティを設定しますがここで設定するcomputedプロパティはreactive関数で定義したuser変数を利用してfirstNameとlastNameを結合して表示するだけのシンプルなものです。
fullNameにカーソルを当てると型推論によりfullNameの型が自動で設定されComputedRef<string>であることがわかります。

明示的に型を指定したい場合はrefのようにジェネリクスを利用して設定を行うことができます。Computedプロパティの引数で設定した関数の戻り値の型を設定します。
関数の型設定
VueのOptions APIではデータプロパティを更新する際にはmethods(メソッド)に関数を追加していましたがCompotions APIではscriptタグの中にそのまま関数を記述することがreactiveな変数を更新することができます。TypeScriptにおける関数の型の設定と方法は同じです。
changeNameを引数なしで定義してuser.firstNameの値を”Jane”に設定し実行します。
ブラウザ上には”Jane Doe”と表示され関数の処理は正常に動作します。
関数の場合は引数と戻り値に対して型の設定を行うことができます。戻り値の設定を行っていない場合でも型推論によって自動で型が設定されます。

changeName関数には戻り値がないため戻り値の型がvoid型になっていることがわかります。戻り値の型を明示的に指定したい場合には下記のように設定することができます。
次に関数に引数を設定します。
引数でnameを指定していますが型の指定がないのでVSCode上ではnameの下に波線が表示されます。メッセージが表示されます。nameにカーソルを合わせるとメッセージには”nameは暗黙的にanyタイプを持っている”と表示されます。

npm run devコマンド上にはエラーのメッセージは発生していませんがTypeScriptの型チェックを行うことができるnpm run typeckeckコマンドを実行してみましょう。npm run typecheckコマンドが実行できることはpackage.jsonファイルのscriptsにtypecheckが登録されていることで確認することができます。実行するとVSCode上に表示されていたエラーと同じ内容が表示されます。
メッセージを見るとわかるように暗黙的にany型が設定されていることが原因でエラーになっています。暗黙的にany型が設定されていると必ずエラーになるわけではなくTypeSciprtのコンパイラオプションのnoImplicitAnyで制御することができます。noImplicitAnyという名前からimplicit(暗黙的)なany型は許可しないということを表しています。これはtsconfig.jsファイルでstrictがtrueになっているためnoImplicitAnyが有効になっています。tsconfig.jsファイルでstrictの値をfalseに設定すると暗黙的なanyが許可されることになりエラーは消えます。
strictの値をtrueに設定することでnoImplicitAnyを含め7つのオプションが有効になります。strictという名前の通り設定を行うと型のチェックが厳格になります。
strictをfalseに変更した後にnpm run typecheckコマンドを実行するとエラーメッセージは消えます。

strictをtsconfig.jsonでtrueに設定している場合は関数の引数には必ず型を指定しないといけないことがわかります。
tsconfig.jsonファイルを更新すると設定値が反映されることが確認できたのでstrictはtrueに戻しておいてください。any型を利用するとどのような値でも入れることができるためコードの品質が下がってしまうのでstrictはtrueに設定しておきます。
関数の戻り値については型推論で型が設定されているため明示的に設定する必要はありませんでしたが引数の場合は明示的に型を設定する必要があります。
引数、戻り値の型を設定しましたが関数についても型を設定することができます。アロー関数と非常によく似ていますが下記のように型を設定することができます。(name:string)=>voidの部分が関数に設定した型を表しています。
Propsの型設定
propsは親子関係を持つコンポーネントでデータの受け渡しを行うために利用する機能です。動作確認するためには別のコンポーネントを作成する必要があります。デフォルトから存在するsrc¥componentsフォルダにあるHelloWorld.vueファイルを利用します。
HelloWorld.vueではpropsの設定が行われているので設定されているpropsを利用して以下のように記述します。
App.vueファイルで作成したHelloWorld.vueファイルをimportしてpropsでmsgを渡します。
ブラウザ上にはHelloが表示されます。
”script setup”とComposition APIでpropsを利用する場合にはdefineProps関数を利用します。refやcomputedと異なりdefineProps関数をimportする必要はありません。defineProps関数はコンパイラーマクロと呼ばれscript setupの中で利用することができます。その他のコンパイラーマクロには後ほど説明するemitで利用されるdefineEmit関数があります。
上記のHelloWorldコンポーネントの設定で確認したようにpropsで渡される変数は<>の中で型を設定することができます。
複数のpropsがある場合の設定方法を確認します。ref関数を利用して定義したuserをpropsで渡します。
type(型エイリアス)やinterfaceなどで設定した型はexportして別のコンポーネントで利用することができます。User型もHelloWorldコンポーネントで利用できるようにexportをつけています。exportを利用して型を再利用できることでオブジェクトのような記述量の多い型を毎回すべて記述する必要がなくなります。
HelloWorld.vueファイルではdefinePropsを使ってpropsのmsgとuserの型を設定します。User型はApp.vueファイルからimportしています。
複数のpropsがある場合は上記のように設定を行うことができます。Propsという名前のinterfaceにまとめています。
Emitの型設定
親コンポーネントから子コンポーネントに値を渡したい場合にpropsを利用することができます。その逆で子コンポーネントから親コンポーネントに対して通知または値を渡したい場合に利用できるのがemitです。
HelloWorld.vueファイルにボタンを追加してclickイベントを利用してchangeName関数を設定します。ボタンをクリックするとchangeName関数が実行されます。propsを通してuserオブジェクトが渡されていますがpropsで渡されたデータを子コンポーネントで更新することはできません。
userオブジェクトに含まれるプロパティを更新したい場合は親コンポーネントであるApp.vueファイルで行う必要があります。emitを利用して親コンポーネントに対して変更したい値を渡すことができます。
emitを利用するためにはdefineEmits関数を利用してemitの設定を行う必要があります。defineEmitsの引数には関数の型の定義(コールシグネチャー)を記述します。eventはemitを実行する時に設定するイベント名を設定します。このイベント名を利用して親コンポーネントで子コンポーネントから送られてくるイベントを識別することができます。第2引数には親コンポーネントに渡す変数(payload:ペイロード)の型を設定します。関数の戻り値はないのでvoid型を設定しています。渡す値がない場合は第2引数は省略することができます。
emitで設定した定義を元にchangeName関数で実行するemitの処理を記述します。イベント名にはdefineEmitsで設定したchangeNameを設定し、親コンポーネントには文字列(string)のJaneを渡します。これで子コンポーネント側のemitの設定は完了です。
子コンポーネントの設定が完了したら親コンポーネントでchangeNameイベントを検知する設定を行います。
changeNameイベントの検知は@changeNameで行うことができます。changeNameイベントを検知するとchangeName関数を実行します。イベント名と関数名を同じにしていますが関数名を変更することは可能です。changeNameの引数にはemitで渡した値が入るため型を同じstringに設定します。
ブラウザで動作確認を行うと”Change Name”ボタンをクリックするとブラウザ上に表示されている”John Doe”が”Jane Doe”に変わります。

reactiveな値を渡す
親コンポーネントに渡していた値が”Jane”に設定していたので固定値ではなく入力した値を渡せるようにinput要素を追加します。input要素に入力した値を取得できるようにv-modelディレクティブでfirstNameを指定します。firstNameはreactiveな変数でref関数を利用して定義します。
firstNameの初期値は””として型はstringに設定しています。親コンポーネントに渡す値はref関数なのでfirstName.valueの形で渡しています。
input要素に任意の名前を入力して”Change Name”ボタンをクリックすると名前が更新されることが確認できます。

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

firstNameの型をstringからRef<string>に変更します。Refを利用する場合はRefをvueからimportする必要があります。
HelloWorld.vueファイルの更新を行なったのでApp.vueでメッセージが表示されます。defineEmitsで関数の型を変更したのでApp.vue側のchangeNameの引数の型を一致させる必要があります。

firstNameの型をstringからRef<string>に変更します。Refのimportも必要になります。値はfistName.valueで取得することができます。
型を正しく設定することでemitでreactiveな変数を送る際には2つの方法があることがわかりました。
emitの別の記述方法
defineEmitsの引数では関数の型を設定していましたがイベント名のみ設定することも可能です。
イベント名が一致するので上記の設定でemitを実行することができますが型の定義を行っていないため引数に数値にしてもVScode上でメッセージで教えてくれるということはありません。
Template Refsの型設定
Vue.jsではDOM要素に直接アクセスを行いたい場合にもref関数を利用することができます。Template Refsでref関数を利用した場合の型の設定を確認します。
App.vueファイルでrefを利用してinput要素にアクセスできるように以下を記述します。アクセスしたい要素にref属性を設定して任意の名前を設定します。ref関数を利用して定義する変数は要素のref属性で指定した名前と同じ名前を設定する必要があります。
上記のコードはJavaScriptでは問題ありませんがTypeScriptで利用するとinput.valueの下に波線が表示されメッセージが表示されます。メッセージの内容は”オブジェクトは ‘null’ である可能性があります。ts(2531)”です。原因は初期値にnullを設定しているためです。型を明示せずにメッセージを解消するためにref関数の初期値をnullから空白に変更します。
input変数にカーソルを当てると型を確認することができます。Ref<any>となっておりジェネリクスにはany型が設定されています。any型はどのような値でも利用することができるためメッセージが解消されます。

変数inputの中にinput要素の情報が含まれているのか確認するためにinput.valueの中身をconsole.logに指定します。
ブラウザのデベロッパーツールのコンソールには<input />が表示され変数inputの中にinput要素が含まれていることが確認できます。
input要素はfocusメソッド、valueプロパティを持っているので下記のように設定することでブラウザで表示した時にその要素にフォーカスが当たりvalueの値をデベロッパーツールのコンソールに表示することができます。
ブラウザのデベロッパーツールのコンソールには”John”が表示されます。any型が設定されているので特に型に関するメッセージが表示されることなく動作することがわかりました。
TypeScriptではDOM要素に型を設定することができるのでany型ではなく明示的に型を設定してみましょう。input要素の型はHTMLinputElementなのでHTMIInputElementを設定します。
HTMLInputElementを設定するとinput変数の型は型推論によってRef<HTMLInputElement | undefined>になりinputの値がundifinedの場合はinput.valueは値を持たないので下記のメッセージが表示されます。

input.valueが値を持つかどうかのチェックをいれることでメッセージは解消されます。
if文による分岐ではなくoptional chainningを利用することでもメッセージは解消されます。
union型を利用してundefinedも明示的に設定することもできます。
ref関数の値が空白ではなく最初に設定していた初期値のnullにした場合も下記のように記述することができます。
HTMLInputElementの型の確認
input要素の型に突然HTMLInputElementが出てきましたがHTMLInputElementの型を具体的に確認するのはネット上で検索することもできますがVSCodeの場合HTMLInputElementにカーソルを合わせて”Ctrl + クリック”すると型の詳細を確認することができます。
HTMLInputElementはHTMLElementをexteds(継承)していることもわかります。

スクロールしていくとvalueプロパティを見つけることができstring型が設定されていることがわかります。
inputの型をHTMLElementに変更するとHTMLElementの型はvalueプロパティを持っていないのでメッセージが表示されます。

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

template refsを利用して直接要素にアクセスする場合、明示的に型を設定する場合はアクセスする要素がどの要素なのかを確認して正しい型を設定する必要があります。
例えばaタグであればHTMLAnchorElementの型を設定することでhrefへのアクセスを行ってもメッセージが表示されることはありません。HTMLElementやHTMLInputElementの型を設定するとプロパティhrefはHTMLElementに存在しませんと表示されます。
template refsでの型設定を通してHTMLElementの型の基礎も理解できるようになりました。
Eventの型設定
Eventに関する型設定について確認していきます。input要素を準備してchangeイベントを設定します。input要素に入力を行いカーソルを外すとhandleChange関数が実行されます。
上記のコードの場合は引数に型を設定してないのでメッセージが表示されます。関数の時に確認しましたが引数に型を設定していない場合はany型が設定されますがTypeScriptの設定ファイルのtsconfig.jsonでstrictをtrueに設定しているため”noImplicitAny”が有効になり暗黙的なanyは利用することはできません。

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

eventのtargetに何が入っているのか確認してみましょう。
ブラウザのデベロッパーツールのコンソールには<input />が入っていることがわかります。input要素の型はtemplate Refsの中で確認した通りHTMLInputElementであったことを思い出しましょう。
changeイベントを実行する要素は必ずinput要素であることがわかっているのでevent.targetには必ずinput要素がはいります。nullを避けるためにevent.targetの型を型アサーションを使ってHTMLInputElementの型を上書きします。
メッセージは消えてinput要素に文字列を入力してカーソルを外すと入力した文字列がデベロッパーツールのコンソールに表示されます。
引数があった場合
handleChange関数に引数があった場合も確認しておきます。以下のようにhandleChangeに$eventを利用することで引数とは別にイベントを取得することができます。
mousemoveイベントの例
mousemoveイベントを使ってマウスの位置情報を取得するコードを利用してEventの型について確認を行います。
eventにEvent型を設定しているのでこれで問題ないように思うかもしれませんがpageXとpageYに波線が表示されます。メッセージにはEvent型はプロパティのXを持っていないといった内容です。VScodeであればEventにカーソルを当て”Ctrl + クリック”をしてEventの中身を確認します。プロパティのtargetやtypeなどを見つけることができますがpageX, pageYはありません。

HTMLElementを継承していたHTMLInputElementやHTMLDivElementがあるようにEventを継承するMouseEvent型が存在します。MouseEvent型を設定するとメッセージの表示は消えます。
MouseEvent型の中身を見るとpageX、pageYを確認することができます。UIEventをextends(継承)していますがUIEventはさらにEventをextendsしています。

Eventといってもさまざまな型が存在するのでイベントに合わせた型を選択する必要があります。
fetch関数の型設定
外部のリソースからfetch関数を利用して取得したデータをv-forディレクティブを利用して展開表示する場合の設定を確認しておきます。外部リソースにはJSONPlaceholerを利用します。無料のサービスでhttps://jsonplaceholder.typicode.com/usersにアクセスすると10名分のユーザ情報を取得することができます。
TypeScriptを利用しない場合は下記のように記述することでブラウザ上に10名分のユーザ名を表示することができます。取得する際にはfetch関数とasync, await関数を利用しています。
このコードをそのままTypeScriptで利用しようとするとv-forディレクティブのkeyのuser.idで”プロパティ ‘id’ は型 ‘never’ に存在しません。”と表示されます。user.nameも同様のメッセージが表示されます。
変数usersにカーソルを合わせるとRef<never[]>で配列のnever型が設定されていることができます。

ref関数に明示的に型を設定します。JSONPlaceHolderから戻されるユーザ情報を確認する必要があります。直接ブラウザのURLにhttps://jsonplaceholder.typicode.com/usersを入力することでユーザ情報を取得することができます。
この情報を元にinterfaceでUserを定義します。戻されるユーザ情報には複数のプロパティがありますがここではコードが長くなるので一部のみ設定を行います。
interfaceのUserを作成後ref関数に型を設定します。本文書で説明済みですがref関数は2つの方法で型を設定することができます。
ref関数に型を設定するとuser.idのメッセージが解消されブラウザ上には取得したユーザ一覧が表示されます。

ユーザ一覧をブラウザ上に表示することができましたがTypeScriptをさらに理解できるようにfetchUsers関数の中での型を確認していきましょう。
fetchUsersにカーソルを当てると型推論により関数の戻り値がPromise<void>であることがわかります。asyncの戻り値はPromiseのジェネリクスに戻り値の型を入れることがわかります。

明示的に型を指定したい場合は下記のように行うことができます。戻り値がないのでvoid型を指定しています。
もしusers.valueのように値を戻す場合はvoid型ではなく戻される型(User[])を設定することになります。
次はawaitで実行されるfecth関数の戻り値を確認するために変数のresにカーソルを当てます。
型推論によりResponse型が設定されていることがわかります。Responseの型とはどのようなものなのでしょう。

明示的に戻り値にResponse型を設定して型の中身を確認してみましょう。resにResponse型を設定した後にVSCodeの場合はResponseにカーソルを当てて”Ctrl + クリック”で型情報が表示されます。
interface Responseの型は下記のように設定されています。Responseはbodyをextends(継承)しているのでBody型の中身も一緒に確認してみましょう。
interfaceを構成するプロパティの中にはstatusやokなど見慣れたものもあるのではないでしょうか。見慣れていない人もresの中身をconsole.logで出力することでReponseのプロパティと変数resに含まれるプロパティの情報が一致することを理解することができます。
ブラウザのデベロッパーツールのコンソールを確認します。interfaceのReponseで定義されているプロパティとresに含まれるプロパティが一致することがわかります。

実際の値と型を比較することでResponse型の理解も深まったかと思います。取得したresオブジェクトからユーザデータを取得するjsonメソッドはInterfaceのBody型に含まれているので戻り値がPromise<any>であることもわかります。
このようにResponseやHTMLElementなど自分で設定を行っていない型を確認していくことでその型を設定した変数がどのようなプロパティやメソッドを持っているのかを確認することができます。型を理解することができればどのような機能を持っているかも理解できるようになっていきます。