TypeScriptでEventの取り扱いがめんどくさ過ぎる。。。
はじめに
Nuxt.js + TypeScriptを今試し中なのだけど、TypeScriptでのイベントの取り扱いが面倒なので、とりあえずStructural Subtypingを適用してみました。
今のところ問題なさそうなんだけど、軽く探して見た限りだとあまり類似のアプローチ無いし悪手なのかな?
てか、FWとか一般的なライブラリで対応してるけど見つけれてないだけの気がしてならない。。。
TL;DR
- Event型を取り扱うときはtargetの型が不明なのでコンパイルエラーになる事がある
-
event: { target: HTMLButtonElement }
型を指定するのが一番手っ取り早い - 求む、もっと手軽な対応方法
TypeScriptでのEvent型の取り扱いの罠
クリックイベント
が発生したHTMLのノードに対して作業したい事は良くあるかと思います。
例えば下記のように 「クリックしたボタンが所属してるツリーのinput型の値を取得したいとき」 とかですね。編集ボタンとかで対象のIDを特定したいようなユースケースです。
<template>
<div class="item">
<input type="hidden" value="1234" />
<button class="btn btnEdit" @click="onMyClick"></button>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
methods: {
onMyClick(event: any) {
console.log(event.target.parentElement.getElementsByTagName("input"));
},
},
});
</script>
ただany
というのもあれなのでせっかくなので型を指定してみたい。というわけで以下のようにEvent型
を指定。
onMyClick(event: Event) {
console.log(event.target.parentElement.getElementsByTagName("input"));
},
これでOKかと思いきや以下のようなコンパイルエラーが。
TS2531: Object is possibly 'null'.
11 | methods: {
12 | onMyClick(event: Event) {
> 13 | console.log(event.target.parentElement.getElementsByTagName("input"));
| ^^^^^^^^^^^^
14 | },
15 | },
16 | });
TS2339: Property 'parentElement' does not exist on type 'EventTarget'.
11 | methods: {
12 | onMyClick(event: Event) {
> 13 | console.log(event.target.parentElement.getElementsByTagName("input"));
| ^^^^^^^^^^^^^
14 | },
15 | },
16 | });
どうやらEvent型だとtarget
の型が確定してないのでコンパイルエラーになるようです。これはちょっと面倒。。。
まあ、any型
のままでも良いと言えば良いし、キャストする手もあるんだけど対応策を一応調べてみました。
instanceofによる解決
調べてみると下記のようにinstanceofでの対応がいくつかありました。
onMyClick(event: Event) {
const { target } = event;
if (!(target instanceof HTMLButtonElement)) {
return; // or throw new TypeError();
}
console.log(target.parentElement?.getElementsByTagName("input"));
},
instanceof
でHTMLInputElement
であることを確認してreturn
または例外でメソッドを終了させればコンパイラがちゃんとtarget
フィールドの型を特定してくれるみたいですね。これは中々賢い! nullの可能性はるのですがそれは?
を使うことで解決。
なのですが、毎回これを書くのは面倒ですしお呪い感が強すぎますね。なんで別な方法を考えてみました。
Structural Subtyping による解決
とりあえずtargetフィールド
の型が明示できれば良さそうなのでEvent型
を継承してStructural typingを使ってみます。
interface HTMLButtonEvent extends Event {
target: HTMLButtonElement;
}
import Vue from "vue";
export default Vue.extend({
methods: {
onMyClick(event: HTMLButtonEvent) {
console.log(event.target.parentElement?.getElementsByTagName("input"));
},
},
});
HTMLButtonEvent型
を作ることでtargetの型が明示されるためコンパイルエラーになりません。良い感じですね。
ただ、これだとHTMLInputElementとかtargetの型ごとににインタフェースを作る必要があって面倒なのでGenerics型に対応させます。
interface HTMLEvent<T extends EventTarget> extends Event {
target: T;
}
import Vue from "vue";
export default Vue.extend({
methods: {
onMyClick(event: HTMLEvent<HTMLButtonElement>) {
console.log(event.target.parentElement?.getElementsByTagName("input"));
},
},
});
これで利用時に型を指定する事でシンプルに対応できます。と、ここまで書いて思ったのですが、単純に以下でも動きますね。
onMyClick(event: { target: HTMLButtonElement }) {
console.log(event.target.parentElement?.getElementsByTagName("input"));
},
継承とかまで書くと毎回定義するのはめんどそうですが、targetを触りたいときに限れば、こっちの方が書くのは楽かもですね。いちいち型定義要らないし。
まとめ
というわけで、一応Nuxt.js + TypeScriptでも型安全にtarget
を取り扱えるようになりました。
今回、TypeScriptで初めてGenerics型を使ったので良い機会になりました。
ただ、車輪の再発明感というか、初歩的な話過ぎて既存のライブラリやFWの対応が無いのが疑わしいんですよね。検索力が足らないだけの気もする。。。
もしくは、この書き方に罠があるか、そもそもユースケースに対して発想が間違ってる可能性が濃厚なので他の人はどうしてるのかが気になる。。。普通にラッパー型が標準でそろえられてそうなものなんだけど。
Discussion
見当外れなコメントだったら申し訳ないのですが、targetの型を連想配列を用いることで指定してあげるとうまくいくようです。
ありがとうございます。見てみますー