なんらかの payload を渡したいとき、インターフェイスを定められない。これに尽きる。静的型付けとも相性が悪い。
payload と言ってるのは要するに window.addEventListener(eventName, event => {})
の event のことだ。TypeScript はかなり頑張って型付けしてくれているが、もし pubsub.addEventListener(event, () => {})
を自作したら型を付けるのがしんどいことは想像に難くない。window.addEventListener もカスタムイベントの event に型をつけるのは面倒だ。
多分ベタープラクティス
ベストではない気がするのでベター。
// 外部にエクスポートしない。 class PubSub { private listeners: { [key in PropertyKey]?: ((payload?: any) => void)[] } = {}; addEventListener(eventName: string, callback: (payload?: any) => void) { if (!this.listeners[eventName]) { this.listeners[eventName] = []; } this.listeners[eventName]?.push(callback); } removeEventListener(eventName: string, callback: (payload?: any) => void) { this.listeners[eventName] = this.listeners[eventName]?.filter( (item) => item !== callback ); } dispatchEvent(eventName: string, payload?: any) { for (const listener of this.listeners[eventName] || []) { listener(payload); } } } const clickEvent = 'BUTTON_CLICK'; const buttonPubsub = new PubSub(); export const buttonObserver = { click: () => buttonPubsub.dispatchEvent(clickEvent), addClickListener: (callback: () => void) => { buttonPubsub.addEventListener(clickEvent, callback); return () => buttonPubsub.removeEventListener(clickEvent, callback); }, };
このように、buttonOberver を介して PubSub を操作することで、addEventListener と dispatchEvent に対して適切な型をつけられる。さらに、PubSub インスタンスを都度生成すれば、他の Observer とカスタムイベント名が衝突しない。
ただし、PubSub クラスを外部へ露出させない都合上、モジュールへの切り出しが行えないデメリットがある。eslint-plugin-import などでアクセス制限を敷いても良いが、規模感次第だろう。また、テストをする際もステートを都度リセットしなければならず、若干コード量が増える。