🛴

TypeScriptでEventの取り扱いがめんどくさ過ぎる。。。

2021/01/24に公開2

はじめに

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での対応がいくつかありました。
https://qiita.com/simochee/items/b3d4dc15f474f805573c
https://yuutookun.hatenablog.com/entry/2018/07/21/084912

    onMyClick(event: Event) {
      const { target } = event;
      if (!(target instanceof HTMLButtonElement)) {
        return; // or throw new TypeError();
      }
      console.log(target.parentElement?.getElementsByTagName("input"));
    },

instanceofHTMLInputElementであることを確認して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