TypeScript で addEventListener のコールバック関数で Event.target
, Event.currentTarget
を使おうとしたら結構めんどかったのでメモ
Event.target と Event.currentTarget の違い
これはそもそも JavaScript の違いですが、Event.target
はイベントが発生した要素を返すので必ずしもイベントリスナーを付けた対象になるとは限りません。
The read-only target property of the
Event
interface is a reference to the object onto which the event was dispatched. It is different fromEvent.currentTarget
when the event handler is called during the bubbling or capturing phase of the event.
cf. Event.target - Web APIs | MDN
// HTML <button><span>Button</span></button> buttonElement.addEventListener('click', (evt) => { evt.target; // => span だったりと button とは限らない });
一方で Event.currentTarget
はイベントリスナーを付けた要素が返されます。
cf. Event.currentTarget - Web API | MDN
// HTML <button><span>Button</span></button> buttonElement.addEventListener('click', (evt) => { evt.currentTarget; // => button });
TypeScript での Event.target
Event.target
の対象は変化するので
const input = <HTMLInputElement>document.getElementById('input'); input.addEventListener('change', (evt) => { evt.target.value; // => Property 'value' does not exist on type 'EventTarget'. ts(2339) });
evt.target
は EventTarget
型で必ずしも HTMLInputElement
が来るわけではないから evt.target.value
が取得できるかどうか判らないので TypeError になります。
なので、evt.target instanceof HTMLInputElement
で Type Guard してあげれば OK
input.addEventListener('change', (evt) => { if (!(evt.target instanceof HTMLInputElement)) { return; } evt.target.value; // OK });
TypeScript での Event.currentTarget
Event.target
は変化するので、React のイベントリスナーでも target
を使っていると currentTarget
を使うように促されます。
Event.currentTarget
はイベントリスナーを付けた要素なので addEventListener
でも…
const input = <HTMLInputElement>document.getElementById('input'); input.addEventListener('change', (evt) => { evt.currentTarget.value; // => Property 'value' does not exist on type 'EventTarget'. ts(2339) });
なんでや!
addEventListener
のコールバック内の evt.currentTarget
は evt.target
と同じ EventTarget
型と判断されてしまい、evt.currentTarget.value
が存在するとは限らないとしてエラーになってしまいました。
XMLHttpRequest object also can be currentTarget but it is not Element/HTMLElement. Maybe that's the reason behind this. Would generic type improve this?
cf. Request to change currentTarget in Event interface for lib.d.ts · Issue #299 · microsoft/TypeScript · GitHub
MDN にも
event.currentTarget
TheEventTarget
whoseEventListeners
are currently being processed. As the event capturing and bubbling occurs, this value changes.
cf. Comparison of Event Targets - Web APIs | MDN
と書かれていました。
どうやら XMLHttpRequest なども currentTarget になることができるので、必ずしも Element
, HTMLElement
とは言い切れないとなっているようです。GitHub の issue も 2014 年に建ててられ Open になったままなので JavaScript 起因の根が深い問題っぽい…
1. instanceof で Type Guard する
Event.target
と同様に Type Guard を使用することで解決ができます。
input.addEventListener('change', (evt) => { if (!(evt.currentTarget instanceof HTMLInputElement)) { return; } evt.currentTarget.value; // OK });
とはいえイベントの度に if 分実行されるの微妙…
2. as でキャストしてしまう
コンパイラより儂の方が正しいんじゃい方法。
input.addEventListener('change', (evt) => { (evt.currentTarget as HTMLInputElement).value; // OK });
キャストだからエラーにはならないよね、、、
3. Event の型を拡張した Event.currentTarget を Generic で渡せる型を作ってしまう
addEventListener のコールバックに渡される引数は Event 型なので、Event.currentTarget
の型を指定できる型で指定すればいい感じになりそうです。
Event.currentTarget
の型を指定できる Event 型の拡張型を作成
interface HTMLElementEvent<T extends EventTarget> extends Event { currentTarget: T; }
これを使えばいい感じに…
interface HTMLElementEvent<T extends EventTarget> extends Event { currentTarget: T; } const input = <HTMLInputElement>document.getElementById('input'); input.addEventListener('change', (evt: HTMLElementEvent<HTMLInputElement>) => { evt.currentTarget.value; });
の筈だったのですが、(evt: HTMLElementEvent<HTMLInputElement>)
の部分でエラーが発生してしまいます。
No overload matches this call. Overload 1 of 2, '(type: "change", listener: (this: HTMLInputElement, ev: Event) => any, options?: boolean | AddEventListenerOptions): void', gave the following error. Argument of type '(evt: HTMLElementEvent<HTMLInputElement>) => void' is not assignable to parameter of type '(this: HTMLInputElement, ev: Event) => any'. Types of parameters 'evt' and 'ev' are incompatible. Type 'Event' is not assignable to type 'HTMLElementEvent<HTMLInputElement>'. Types of property 'currentTarget' are incompatible. Type 'EventTarget' is missing the following properties from type 'HTMLInputElement': accept, align, alt, autocomplete, and 326 more. Overload 2 of 2, '(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void', gave the following error. Argument of type '(evt: HTMLElementEvent<HTMLInputElement>) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'. Type '(evt: HTMLElementEvent<HTMLInputElement>) => void' is not assignable to type 'EventListener'. Types of parameters 'evt' and 'evt' are incompatible. Type 'Event' is not assignable to type 'HTMLElementEvent<HTMLInputElement>'.ts(2769)
なんでや!!!
エラーが長ぇ…
イベントリスナーのコールバック関数ををオブジェクト指定にすればエラーにならない
イベントリスナーには、コールバック関数を指定することもできますが、
EventListener
を実装したオブジェクトを指定することもでき、その場合はhandleEvent() (en-US)
メソッドがコールバック関数として機能します。
コールバック関数自体は、handleEvent()
メソッドと同じ引数と返値を持ちます。つまり、コールバック関数は発生したイベントを説明するEvent
に基づいたオブジェクトを唯一の引数として受け付け、何も返しません。
cf. EventTarget.addEventListener() - Web API | MDN
先のスクリプトを下記のように変更すれば OK
interface HTMLElementEvent<T extends EventTarget> extends Event { currentTarget: T; } const input = <HTMLInputElement>document.getElementById('input'); input.addEventListener('change', {handleEvent: (evt: HTMLElementEvent<HTMLInputElement>) => { evt.currentTarget.value; }});
これで Event.currentTarget
の型問題が解決しました!
ただ見慣れない書き方なので、素直に Type Guard した方が良いのかも知れない… TypeScript なんもわからん
所管
Event.currentTarget でここまで苦労するとは思わなかったよ。パトラッシュ…
と TypeScript の <T>
これ Generic なのか Generics なのか、読み方も ジェネリック なのか ジェネリックス なのか ジェネリクス なのか分からん。(英語見た目と読み方が違うやつ多くてムズい…)
[参考]
- イベントオブジェクトの target と currentTarget の違いとそれに伴う TypeScript の挙動を調べてみた
- Event.target - Web APIs | MDN
- Event.currentTarget - Web APIs | MDN
- Comparison of Event Targets - Web APIs | MDN
- TypeScriptでEventの取り扱いがめんどくさ過ぎる。。。
- TypeScriptのaddEventListenerで苦しんだところまとめ - Qiita
- TypeScriptのaddEventListenerで「EventTargetの中にそんなプロパティないよ」と怒られる問題について - Qiita
- TypescriptでaddEventListener時のEventTarget型ではまった - Qiita