🎯

TypeScript で EventTarget を継承する方法

2024/05/11に公開

実際のコード

TypeScript において EventTarget を継承したクラスを作成するコードは以下のように書くことができます。

/**
 * @file Pure Typed EventTarget Example
 * @license Unlicense
 */

declare class TypedEventTarget<EventMap extends Record<string, Event>>
  extends EventTarget {
  addEventListener<Type extends keyof EventMap>(
    type: Type,
    listener: (this: this, evt: EventMap[Type]) => void,
    options?: boolean | AddEventListenerOptions
  ): void;
  addEventListener(
    ...args: Parameters<EventTarget["addEventListener"]>
  ): void;
  removeEventListener<Type extends keyof EventMap>(
    type: Type,
    listener: (this: this, evt: EventMap[Type]) => void,
    options?: boolean | EventListenerOptions
  ): void;
  removeEventListener(
    ...args: Parameters<EventTarget["removeEventListener"]>
  ): void;
}

// TypeScript 4.7+
export class Foo extends (EventTarget as typeof TypedEventTarget<{
  foo: CustomEvent<string>;
  bar: CustomEvent<number>;
  baz: CustomEvent<{ ok: boolean }>;
}>) { }

// TypeScript 4.6
export class Foo extends (EventTarget as {
  new (): TypedEventTarget<{
    foo: CustomEvent<string>;
    bar: CustomEvent<number>;
    baz: CustomEvent<{ ok: boolean }>;
  }>
}) { }

上記 TypeScript コードをコンパイルすると以下の JavaScript コードが得られます。

export class Foo extends EventTarget { }

どうしてこうなった

単に EventTarget を継承しただけでは addEventListener の型推論が思うように効かず使い勝手が悪くなってしまいます。

上記 TypedEventTarget を使用すると以下のように Foo クラスを使用することができます。

new Foo().addEventListener("baz", e => {
//                         ^^^^^ "foo"、"bar"、"baz" のオートコンプリートが効く
  e.detail;
//  ^^^^^^ 型推論が効いて { ok: boolean } 型になる
})

関連項目

Discussion