イベントエミッター は、JavaScriptでポピュラーなデザインパターンです。
ここでは typescript の型システムを用いてより安全に使用する方法を説明します。
イベントエミッターについて
イベントエミッターについて簡単に説明しておきます。
イベントエミッターは、「イベント駆動のプログラミング」を行う際に使われるデザインパターンで、「イベント駆動のプログラミング」とは、順次に処理が実行されるのではなく、なんらかのイベントによってあらかじめ指定された処理が実行されるようなプログラミングの手法です。
使い方
イベントを送信(発火)させる emit メソッドと送信(発火)されたイベントを受け取り、何かの処理をする on メソッドを使います。
// イベントを送信します
emit(eventName:string, value:unknown): void
// イベントが送信されたときに何かを行います
on(eventName: string, f:(value: unknown) => void): void
基本的な例
// イベントエミッターを読み込む
let EventE = require('events').EventEmitter;
// インスタンスを作成する
let eveIns = new EventE();
//*********************************
// 例1
//*********************************
// イベントを受け取る側--------------
eveIns.on('event', data => {
// イベント発火後の処理を記述
console.log('onメソッド:', data);
})
// イベントを受け取る側(1回限り)
eveIns.once('event', data => {
// イベント発火後の処理を記述
console.log('onceメソッド:', data);
});
// イベントを送信(発火)側----------
eveIns.emit('event', 1);
eveIns.emit('event', 2);
eveIns.emit('event', 3);
// 出力
// onメソッド: 1
// onceメソッド: 1
// onメソッド: 2
// onメソッド: 3
//*********************************
// 例2
//*********************************
eveIns.on('ready', data => {
console.log('ready:', data)
})
eveIns.on('error', e => {
console.error('error:', e.message)
})
eveIns.on('reconnecting', paras => {
console.error('reconnecting:', paras)
})
eveIns.emit('ready', 1)
eveIns.emit('error', new Error('エラー!'))
eveIns.emit('reconnecting', { attempt: 10, delay: 20 })
// 出力
// ready: 1
// error: エラー!
// reconnecting: { attempt: 10, delay: 20 }
型付けして安全に使う
上記サンプルコードの例2を型付けしていきたいと思います。
以下は、イベント名とコールバックの引数の型の定義です。
type Events = {
ready: void
error: Error
reconnecting: { attempt: number, delay: number }
}
次に on メソッド に Events 型をマッピングしていきます。
type EventsMap = {
on<E extends keyof Events>(
event: E,
f: (arg: Events[E]) => void
): void
}
// 上記は以下のような展開になります。
// type EventsMap = {
// on<E extends 'ready' | 'error' | 'reconnecting'>(
// event: E,
// f: (arg: Events[E]) => void
// ): void
// }
// ▼
// ▼
// // 型がオーバーロードされている
// type EventsMap = {
// on(
// event: 'ready',
// f: (arg: Events['ready']) => void
// ): void
// on(
// event: 'error',
// f: (arg: Events['error']) => void
// ): void
// on(
// event: 'reconnecting',
// f: (arg: Events['reconnecting']) => void
// ): void
// }
// ▼
// ▼
// type EventsMap = {
// on(
// event: 'ready',
// f: (arg: void) => void
// ): void
// on(
// event: 'error',
// f: (arg: Error) => void
// ): void
// on(
// event: 'reconnecting',
// f: (arg: { attempt: number, delay: number }) => void
// ): void
// }
同じように emit メソッドにもマッピングします。
type EventsMap = {
on<E extends keyof Events>(
event: E,
f: (arg: Events[E]) => void
): void
emit<E extends keyof Events>(
event: E,
arg: Events[E]
): void
}
エミッターがこの方法で型付けされていると、キーをスペルミスしたり、引数を間違って入力したり、引数を渡し忘れてしまったりすることがありません。リッスンする可能性のあるイベントと、それらのイベントのコールバック内のパラメーターの型を、typescript のコードエディターが提示してくれます。
型を使ってみる
let EventE = require('events').EventEmitter;
let eveIns:EventLListenerMap = new EventE();
// 定義していないイベント名なのでエディタから警告
eveIns.on('event', arg => {
console.log(arg)
})
// コールバックの引数の型が違うのでエディタから警告
eveIns.on('ready', arg => {
console.log(arg.message)
})
// OK(警告なし)
eveIns.on('error', arg => {
console.log(arg.message)
})
// OK(警告なし)
eveIns.emit('ready', undefined)
// 第2引数の型が違うのでエディタから警告
eveIns.emit('error', 1)
// 第2引数の型が違うのでエディタから警告
eveIns.emit('reconnecting', { attempt: "10", delay: "20" })
// OK(警告なし)
eveIns.emit('reconnecting', { attempt: 10, delay: 20 })
上記のように型付けする事によって、間違えた記述をした場合エディタから警告が発せられるので、より安全に、正確に使う事ができます。
まとめ
全コードです。
type Events = {
ready: void
error: Error
reconnecting: { attempt: number, delay: number }
}
type EventLListenerMap = {
on<E extends keyof Events>(
event: E,
f: (arg: Events[E]) => void
): void
emit<E extends keyof Events>(
event: E,
arg: Events[E]
): void
}
let EventE = require('events').EventEmitter;
let eveIns:EventLListenerMap = new EventE();
// 定義していないイベント名なのでエディタから警告
eveIns.on('event', arg => {
console.log(arg)
})
// コールバックの引数の型が違うのでエディタから警告
eveIns.on('ready', arg => {
console.log(arg.message)
})
// OK(警告なし)
eveIns.on('error', arg => {
console.log(arg.message)
})
// OK(警告なし)
eveIns.emit('ready', undefined)
// 第2引数の型が違うのでエディタから警告
eveIns.emit('error', 1)
// 第2引数の型が違うのでエディタから警告
eveIns.emit('reconnecting', { attempt: "10", delay: "20" })
// OK(警告なし)
eveIns.emit('reconnecting', { attempt: 10, delay: 20 })
WindowEventMap
以下は WindowEventMap の型付けしているコードです。
interface WindowEventMap extends GlobalEventHandlersEventMap {
// ...
contextmenu: PointerEvent
dblclick: MouseEvent
devicelight: DeviceLightEvent
devicemotion: DeviceMotionEvent
deviceorientation: DeviceOrientationEvent
drag: DragEvent
// ...
}
interface Window extends EventTarget, WindowTimers, WindowSessionStorage, WindowLocalStorage, WindowConsole, GlobalEventHandlers, IDBEnvironment, WindowBase64, GlobalFetch {
// ...
addEventListener<K extends keyof WindowEventMap>(
type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void
removeEventListener<K extends keyof WindowEventMap>(
type: K, listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | EventListenerOptions
): void
}