mittをTypeScriptで怒られずに使う(overloadとの付き合い方の例)

EventEmitter mitt が便利

developit/mitt: 🥊 Tiny 200 byte functional event emitter / pubsub.

最近あんまり聞かなくなったけど、JavaScript はもともとイベントドリブンである。Promise や async/await は便利ではあるけど、いつ終わるか分からない処理に対して途中の状態を伝えてくれるものとか、イベントそのもので扱えばより自由度が増すので、個人的には単に UI イベントハンドラを書くだけでなく、イベントで設計することに慣れておくに越したことはないと思っている。

mitt については、Vue.js が 2 から 3 に上がる際に Vue の中と外を結ぶ EventBus を独自に提供するのをやめるから mitt とか使え、と言い始めて以来利用している。

Events API | Vue 3 Migration Guide

小さいし気が利いてるし、気に入っている。

mittのtypeがちょっと複雑

ただ、mitt の type が実はやや複雑で、定義をそのまま抜き取ると以下のようになっている。

mitt 3.0.1

export interface Emitter<Events extends Record<EventType, unknown>> {
  all: EventHandlerMap<Events>;
  on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
  on(type: '*', handler: WildcardHandler<Events>): void;
  off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): void;
  off(type: '*', handler: WildcardHandler<Events>): void;

なんのこっちゃと思うかもしれないけど、これ overload を使っている。JavaScript には overload なんてものはないんだけど、TypeScript には overload があって、mitt はその overload を利用している。

具体的に何がややこしさになるかというと、以下のようなコードを考えると分かりやすい。

class Klass {
  bus

  on (event: string, handler: Function) {
    this.bus.on(event, handler) // <- mismatch
  }
}

みたいにして mitt を内部に持つ何かを書いて、外で

(new Klass()).on('event', (e) => process)

みたいなことをやりたいと思っても、上の Klass 内の on メソッドの段階で以下のように怒られる。

No overload matches this call.
  Overload 1 of 2, '(type: "*", handler: WildcardHandler<Events>): void', gave the following error.
    Argument of type 'string' is not assignable to parameter of type '"*"'.
  Overload 2 of 2, '(type: keyof Events, handler: Handler<string>): void', gave the following error.
    Argument of type 'string' is not assignable to parameter of type 'keyof Events'.ts(2769)

mitt はイベントを '*' で listen すると全部の event を扱える便利機能があるんだけど、'*' とそれ以外は定義が分かれていて、便利機能の部分は overload で実現されていて、string を受け取るパターンは '*' に該当しない。

その on に対して上の例は event を string で決め打ちにしているので type が合わないぞと言っている。

解決策

同じようにoverloadを定義する

頑張ればできなくはない。公式のドキュメントに従うならこの方向になりがち。

import mitt, { Emitter, WildcardHandler, Handler } from 'mitt'

type Events = {
  yet: string
  processing: string
  done: string
}

class XXXXX {
  ..
  on (event: '*', handler: WildcardHandler<Events>): void
  on <Key extends keyof Events>(event: Key, handler: Handler<Events[Key]>): void
  ..

だけど、もっと安直な方法もある。

もっと雑にしちゃう

現実的にはこんな感じでも十分イケる。

import mitt, { type Handler } from 'mitt'

class XXXXX {
  ..
  on (event: string, handler: Function): void {
    this.bus.on(event as any, handler as unknown as Handler)
  }
  ..
}

これで外からは string と Function を与えればいいんだなと分かるし、内部ではよしなに緩くして移譲することができる。

string 以外も受け取りたいです、ってなったら話は変わるけど、全部 string でも実用的には十分なので、こんな感じで避けてしまってもよいかなと思う。この場合は EventEmitter を「利用する」側にとっては overload の厳密さは逆に使い勝手の悪さに繋がっているのではないかと思う。

More