🔍

【TS】型推論が効かないEventオブジェクトへの対処法

2023/06/16に公開

TypeScriptで必要不可欠な機能と言えば型推論

しかし、中には型推論が上手く機能しない事例も存在します。
例えば、タイトルにあるeventオブジェクトなどです。

今回はこのeventオブジェクトを用いて、
型推論が上手く機能しない原因と対処法についてご紹介します。

Eventオブジェクトとは

eventオブジェクトはJavaScriptに備わるオブジェクトで、
特定のイベントが発生した際の詳細情報を提供するオブジェクトになります。
このオブジェクトは、イベントハンドラー(イベントに対するリスナーとして登録した関数)へ自動的に渡されるという特徴を持ちます。

例えば、HTML上に配置されたボタンをマウスでクリックした時を考えてみます。
コードで示すと以下の通りです。

app.js
const handleClick = (event) => {
  if (event) {
    console.log(event)
  }
}

const button_1 = document.getElementById('button-1')
if(button_1) {
  button_1.addEventListener('click', (event) => handleClick(event))
}

上記のコードでは、button_1に対してclickイベントリスナーが付与されており、
イベントハンドラーとしてhandleClick関数が設定されています。

そして、handleClick関数に渡されている引数がeventオブジェクトです。
eventオブジェクトはイベントが発生した際 (今回の場合はマウスでボタンをクリックした際),
自動的にイベントハンドラーであるhandleClick関数へ渡される事になります。

具体的にどのような情報がeventオブジェクトへ渡されるかについては、発生したイベントの種類によって異なります。
例えば、マウスによるクリックの際はMouseEventがeventオブジェクトとして渡されます。

※他のEventについても知りたい方は、下記リンク先をご参照ください。
イベント一覧(MDN Web Docs)

実際に上記コードを実行してみると、下図のようにコンソール上にてeventの中身が確認できます。

Eventオブジェクトの型について

型推論をした場合

上述したapp.jsのコードを、app.tsへと書き換えたいと思います。
この時、handleClick関数の引数として渡されるeventにはどのような型を当てはめればよいでしょうか。

対象となるeventをマウスオーバーして、型推論を行ってみます。

型推論の結果、eventMouseEvent型ということが分かりました。
よって、eventの型として下記のようにMouseEventを当てはめてあげれば、
JS⇒TSへの変換が完了となります。

app.ts
const handleClick = (event: MouseEvent) => {
  if (event) {
    console.log(event)
  }
};

const button_1 = document.getElementById('button-1')
if(button_1) {
    button_1.addEventListener('click', (event: MouseEvent) => handleClick(event))
};

型推論で上手くいかない場合

上記のコードを少し書き換えてみます。
クリックした際にeventの中身をコンソール上に出力するのではなく、
ボタン内に記載されている"1"を出力するとします。

先ほどのconsole.log(event)の出力を利用して、
ボタンの"1"がMouseEvent内のどのプロパティに保持されているのかを確認します。

すると、MouseEventのtargetプロパティ内にあるinnerHTMLプロパティにて
ボタンに記載されている"1"が保持されている事が分かります。

よって、先ほどのapp.tsは以下のように書き換えられるはずです。

app.ts
const handleClick = (event: MouseEvent) => {
  if (event) {
+    console.log(event.target.innerHTML)
-    console.log(event)
  }
};

const button_1 = document.getElementById('button-1')
  if(button_1) {
    button_1.addEventListener('click', (event: MouseEvent) => handleClick(event))
};

しかし、event.target.innerHTMLと書き換えると、
型が勝手にEventTarget型であると解釈され、EventTarget型にはinnerHTMLプロパティは存在しないというエラーが出てきてしまいます。

Eventオブジェクトにて型推論が上手く機能しない原因

eventの型として型推論ではMouseEvent型と出ていて、MouseEvent内に実際にinnreHTMLプロパティがあるにも関わらず、上記のコードでエラーとなってしまうのはなぜでしょうか。

その答えは、TypeScriptで標準実装されている型定義ファイルにて、MouseEventの全てのプロパティが型定義されていないからとなります。

実際に、MouseEventが標準の型定義ファイルでどのように型定義されているかを確認してみたいと思います。




MouseEventの型定義内には、targetプロパティは見当たりません。
MouseEventはUIEventの型定義を継承している(MouseEvent extends UIEventの部分)ので、今度はUIEventを確認してみます。



UIEventの型定義内でもtargetプロパティは見当たりません。
同様に継承元であるEventプロパティを調べてみます。


Eventの型定義内でようやくtargetプロパティの型定義を発見しました。
どうやら、targetプロパティはEventTarget型またはnull型と定義されているようです。
基本的に型推論は標準実装の型定義ファイルを基に行われるので、
MouseEventにてtargetを用いると、その型はEventTarget型またはnull型と型推論されることになります。
前節のエラーメッセージの中で突然出てきたEventTargetですが、EventTargetevent.targetが型推論されて導出されたものという事が分かります。
では、EventTargetはどのように型定義されているのかを確認してみます。


EventTargetの型定義内ではinnerHTMLプロパティは見当たりません。
まさに前節のエラーメッセージの通り、EventTargetにはinnerHTMLプロパティが無いという状況です。
EventTargetは何も継承していないので、ここまででMouseEventの型定義は出揃ったことになります。すなわち、MouseEventの型定義内には、innerHTMLプロパティが定義されていないという事が分かります。

※Eventオブジェクトにおいてプロパティが全て定義されていない理由

Eventオブジェクトの概要について説明した際にご紹介した通り、Eventオブジェクトには主にイベントが発生するまで確定しない情報が渡される事になります。
イベントが発生するタイミングや状況(マウスで入力するかキーボードで入力するか等)でプロパティが変動する可能性があるため、一部のプロパティについては標準で型定義が出来なかったのだと思われます。

対処法

上述のケースのように型推論では上手くいかない場合、
対処法の一つとして型アサーション(型の強制)を利用するというものがあります。

app.ts
const handleClick = (event: MouseEvent) => {
    if (event) {
+        console.log((event.target as Element).innerHTML) 
-        console.log(event.target.innerHTML) 
    }
};

const button_1 = document.getElementById('button-1')
if(button_1) {
    button_1.addEventListener('click', (event: MouseEvent) => handleClick(event))
};

上記のコードでは、event.targetの型をElement型であると指定しています。
標準の型定義ファイル(lib.dom.d.ts)にてInnerHTMLがどのように型定義されているか調べると、InnerHTMLは個別で型定義されておりElementの型定義へ継承されていることが分かります。

よって、event.targetの型をElement型であると指定することにより、
エラーを回避しています。

まとめ

今回は、TypeScriptにてEventオブジェクトを扱う際の対処法についてご紹介しました。
参考文献を眺めていると、この問題については様々な方が頭を悩ませているようで、自分だけではないのだと少しホッとしました...
この記事では型アサーションによる解法しかご紹介出来ませんでしたが、他にも方法があるようで、まだまだ理解出来ていないところが沢山あるなと感じました。
少しずつ精進したいなと思います。

参考文献

TypeScript Event.target, Event.currentTarget の型がむずい!
【TypeScript】Reactでのイベントに設定する型の理解が少しだけ深まる
TypeScriptでEventの取り扱いがめんどくさ過ぎる。。。

Discussion