EventEmitterをTypeScriptで迷いなく使う

EventEmitter、フラッシュメッセージ[^1]を書く時とかに便利ですよね

![IMG_0164のコピー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106711/50013cb3-85c9-60d0-243b-9e1bef3b1e7d.jpeg)

長々と親コンポーネントから、フラッシュメッセージを呼び出すためのメッセージをバケツリレーで渡す必要もないので、コードが綺麗にまとまります。

これにTypeScriptを使って型定義をしてあげるともっと快適に、型安全に使えたのでまとめてみました

# 舞台設定

例として、今回は２つのアクションをEventEmitterに登録する場合を考えてみました。

- 名前を引数に渡して、「`Hello 名前`」を出力するだけのアクション
- メッセージと（オプションで）関数を渡して、フラッシュメッセージを出すアクション

ポイントは２つの関数で、「**渡す引数が個数も型も違う**」ということです。

もし型を定義しないと

- EventEmitterって、全部でどんなアクションがあったっけ？:thinking:
- このアクションに渡す引数ってこれであってたっけ？:thinking:

みたいなことになりかねませんが、型を定義すればそんな杞憂ともおさらばです！

ではさっそく書いてみましょう！

# 用意するコード

２種類あります

- EventEmitterに登録する全てのアクションの型定義
- アクションがemitされた時に、トリガーされるロジック

なお、前提としてこれら二つはアプリ全体から参照できる場所で定義するものとします（`create-react-app`なら`src/App.js`）

# EventEmitterのアクション型定義

まず確認しておきますと、`EventEmitter.emit`というメソッド自体は「一つ」です。

そのメソッドに対し、TypeScriptの型を複数個定義してあげることで、あたかも「複数のメソッドを定義」しているかのように振舞わせることができます

（大元のEventEmitter.emitのドキュメントは[こちら](https://nodejs.org/api/events.html#events_emitter_emit_eventname_args)）

```typescript
// myEventEmitter.ts
import { EventEmitter as OriginalEventEmitter } from 'events'

// アクション一覧。必ずオーバーロードしている型と一致させること！！
type EventType = 'flash' | 'greeting'

export interface FlashAction {
  label: string
  onClick: () => void
}

class EventEmitter extends OriginalEventEmitter {
  // 使うアクションの型をオーバーロード
  public emit(
    event: 'flash',
    msg: string,
    action?: FlashAction,
  ): boolean
  public emit(event: 'greeting', name: string): boolean

  // メソッド本体。元々のemitメソッドを踏襲している
  public emit(event: EventType, ...args: any[]) {
    return super.emit(event, ...args)
  }
}

export const eventEmitter = new EventEmitter()
```

# EventEmitterアクションの発生時のアクション
私はロジックを、引数に`eventEmitter`を渡す形のカスタムフック[^2]にまとめています

ここで`eventEmitter.addListener`を書きます

:warning: 残念ながら現時点では、`addListner`内にまでさっき定義した型を適応する方法がありません...。なので、アクションの修正をする際には、「`eventEmitter`定義元」と「`addListener`」、両方を手動で直す必要があります

```typescript
import { EventEmitter } from 'events'

import { useEffect } from 'react'

export const useGreeting = (eventEmitter: EventEmitter) => {
  useEffect(() => {
    eventEmitter.addListener('greeting', (name: string) => {
      console.log('hello', name)
    })
  }, [eventEmitter])
}
```

```typescript
import { EventEmitter } from 'events'

import { useEffect, useState } from 'react'

import { FlashAction } from 'src/config/eventEmitter'

// フラッシュ表示は３秒間
const FLASH_INTERVAL_SEC = 3

export const useFlash = (eventEmitter: EventEmitter) => {
  const [flashAction, setFlashAction] = useState<FlashAction>()
  const [flashMsg, setFlashMsg] = useState('')
  const [isFlashOpen, setIsFlashOpen] = useState(false)

  // 自動でフラッシュメッセージが消えるロジック
  useEffect(() => {
    if (!isFlashOpen) return

    setTimeout(() => {
      setIsFlashOpen(false)
    }, FLASH_INTERVAL_SEC * 1000)
  }, [isFlashOpen])

  useEffect(() => {
    eventEmitter.addListener(
      'flash',
      (msg: string, action?: FlashAction) => {
        setFlashAction(action)
        setFlashMsg(msg)
        setIsFlashOpen(true)
      },
    )
  }, [eventEmitter])

  return { flashAction, flashMsg, isFlashOpen, setIsFlashOpen }
}
```

# これらのロジックを上位階層に配置する

あとはこんな感じでロジックを呼び出します。

flashメッセージの表示には当然コンポーネントも必要なので、こちらも同じく上位階層に定義します
（今回は省略しますが、ニーズがあれば書きます）

```typescript
import { eventEmitter } from './myEventEmitter.ts'
import { useFlash } from './useFlash'
import { useGreeting } from './useGreeting'

useGreeting(eventEmitter)

const {
  flashAction,
  flashMsg,
  isFlashOpen,
  setIsFlashOpen
} = useFlash(eventEmitter)

// こんな感じのコンポーネントがあるとして
return (
  <FlashMessage
    flashAction={flashAction}
    flashMsg={flashMsg}
    isFlashOpen={isFlashOpen}
    setIsFlashOpen={setIsFlashOpen}
  />
)
```

あとは任意のコンポーネントから以下のように呼び出せばアクションが起こせます！ :beers:
当然型チェックつき！ :beers: 

```typescript
import { eventEmitter } from './myEventEmitter.ts'

eventEmitter.emit('greeting', 'john')

// actionはあってもなくてもいい
eventEmitter.emit('flash', 'saved!')
eventEmitter.emit('flash', 'download ready!', () => { downloadCSV() })
```

# 終わりに
いかがでしたでしょうか？

型を定義するだけで、一気に使いやすくなるのですごいおすすめです！

皆さんもぜひTypeScriptで良いEventEmitを！

[^1]: ユーザーのアクションに対するフィードバックとして一時的に表示されるメッセージ。例として、設定の保存ボタンを押したあとに「保存されました」と表示するなどがある
[^2]: React hookを用いて作られた、再利用可能なひとまとまりのロジック。ここでは「ロジックのカプセル化」と「ラベリング」を目的として使っている。[React公式](https://ja.reactjs.org/docs/hooks-custom.html)では「独自フック」と定義されている。