皆様どうも、こんにちは!
こまりのフロントエンドもやってるエンジニア、桑木です。
先日、TypeScriptでReactのイベントの型について考えさせられたので、そこで調べたことなんかを記事にしておきます。
この記事の内容
前提知識
- 基礎的なTypeScriptとReactの構文に関する知識
- ネイティブなDOMのイベントシステム、特にバブリングという挙動について
- ReactのイベントシステムやSyntheticEventについて
普通に型指定してみる
select要素を表示して、項目が選択されたらその値をconsoleに出力するだけのコンポーネントを作成します。
const SomethingComponent: React.FC = (props) => {
const handleChange: React.ChangeEventHandler<HTMLSelectElement> = (ev) => {
console.log(ev.target.value);
};
return (
<div>
<select onChange={handleChange}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</div>
);
};
ここではイベントハンドラに型指定していますが、型推論によってev
はReact.ChangeEvent<HTMLSelectElement>
になります。
const handleChange = (ev: React.ChangeEvent<HTMLSelectElement>) => {
こう書いても結果的に互換性のある型に推論されるので、イベントハンドラとして使用できます。
バブリングしたイベントをキャッチする
この例のコードではわざわざバブリングを利用する意味はありませんが、select要素の親のdiv要素でバブリングしてきたイベントをキャッチするとしたら次のようになります。
const SomethingComponent: React.FC = (props) => {
const handleChange: React.ChangeEventHandler<HTMLDivElement> = (ev) => {
if (!(ev.target instanceof HTMLSelectElement)) {
return;
}
console.log(ev.target.value);
};
return (
<div onChange={handleChange}>
<select>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</div>
);
};
イベントハンドラのジェネリクスに指定している型がHTMLDivElement
に変わっているのと、if文でev.target
がHTMLSelectElement
かどうか判別する処理が加わっています。
イベントハンドラのジェネリクスにはイベントハンドラを設定する要素を指定する
イベントハンドラのジェネリクスがHTMLSelectElement
のままだと、div
のonChange
に割り当てることができない、というエラーが出ます。Reactの型定義を参照すればわかりますが、イベントハンドラのジェネリクスはcurrentTarget
の型を指定することになっています。
イベントオブジェクトのtarget
プロパティはイベントが発生した要素、currentTarget
プロパティはイベントをキャッチした要素=イベントハンドラを設定した要素です。
つまり、イベントハンドラのジェネリクスにはonChange
等でイベントハンドラを実際に設定する要素を指定する必要があるということです。
なぜtargetの型指定ができないのか
このコードでは、if文でev.target
がHTMLSelectElement
型以外の時は関数を抜けています。こうすることで、その後のev.target
はHTMLSelectElement
型として認識されます。これによってev.target.value
プロパティにアクセスすることができます。
ところで、こんな面倒なことをしなくてもジェネリクスなどでev.target
の型を指定できないのでしょうか。
結論から言うと、指定できません。実はこれもReactの型定義にその理由がコメントで書かれているのですが、currentTarget
はイベントハンドラを設定した要素しかありえないのに対して、target
はその子孫に含まれるどの要素の可能性もあるので、コンパイル時にtarget
の型を決定する意味があまりないからです。
changeイベントに関してだけ言うならtarget
はEventTarget
とジェネリクスで指定した型の交差型になっているので、もとのバブリングしていない方のコードでは何の判別もなしにvalue
プロパティにアクセスできています。changeイベントを発生させる要素には確実にvalue
プロパティがあるのでこれで良いのでしょう。
そもそもcurrentTargetを使おう
実際問題として、バブリングの仕組みを積極的に使うことはそんなにありません。通常はイベントハンドラを設定した要素でイベントが発生することを期待します。
currentTarget
を使うことでバブリングを巧みに利用したコードでないことが示せるので、なるべくcurrentTarget
を使用するコードを記述した方が良いのかもしれません。