TypeScript 2.1 で導入される `keyof` を使って `EventEmitter` を定義してみる

# 追記　(2018/09/05)

現在では、 TypeScript 2.8 で導入された Conditional Types を利用したよりよい方法があり、これを使うと最後に書いた「この方法の制限」も克服できます。

https://medium.com/@bterlson/strongly-typed-event-emitters-2c2345801de8

上の記事の内容はかなり難易度が高いですが、これを理解しておくと Conditional Types への理解がかなり深まるので、おすすめです。（途中で僕のことにも言及してくれていて、ちょっと嬉しかったです）

追記終わり。

# `keyof` の概要

issue: https://github.com/Microsoft/TypeScript/issues/1295
PR: https://github.com/Microsoft/TypeScript/pull/11929

PR の説明文を省略して簡単に示すと、以下のような機能。

```ts
interface Thing {
    name: string;
    width: number;
    height: number;
    inStock: boolean;
}

type K1 = keyof Thing;  // "name" | "width" | "height" | "inStock"

type P1 = Thing["name"];  // string
type P2 = Thing["width" | "height"];  // number
type P3 = Thing["name" | "inStock"];  // string | boolean
```

issue の milestone は TypeScript 2.1.2 になっていますが、 roadmap では 2.1 の追加機能の一覧に含まれていて、 `npm i typescript@next` で落としてきた 2.1.0-dev.20161104 にすでに含まれていたので、 2.1.0 ですぐ使えるのではないかと思います。
https://github.com/Microsoft/TypeScript/wiki/Roadmap#21-november-2016

というか 2.1 は今月リリースなのか...!

# `keyof` の何が嬉しいのか

元になった issue では、 例として、 Backbone や Immutable.js や `_.pluck` (lodash では今は `_.map`) での利用が挙げられています。
例えば Immutable.js の Map は、この機能を使うと以下のように定義できます。

```ts
declare module ImmutableJS {
  fucntion Map<T>(obj: T): Map<T>;
  interface Map<T> {
    get<K extends keyof T>(prop: K): T[K];
    set<K extends keyof T>(prop: K, value: T[K]): Map<T>;
  }
}

const map = Immutable.Map({ name: 'François', age: 20 });
map = map.set('age', 21);
map.get('age'); // 21

map.set('name', 21); // error!
```

property 名と値の型の整合性を、コンパイラがチェックしてくれるというわけです。

# `EventEmitter` を `keyof` を使って定義する

個人的には、 EventEmitter をこの機能と組み合わせて使うのがすごく胸熱というか、それを楽しみにこの機能の追加を首を長くして待っていたので、ここで説明しようと思います。

## EventEmitter と型定義

まず、現在の DefinitelyTyped types-2.0 branch の EventEmitter の型定義を見てみましょう。

```ts
export class EventEmitter {
    addListener(event: string | symbol, listener: Function): this;
    on(event: string | symbol, listener: Function): this;
    once(event: string | symbol, listener: Function): this;
    removeListener(event: string | symbol, listener: Function): this;
    removeAllListeners(event?: string | symbol): this;
    setMaxListeners(n: number): this;
    getMaxListeners(): number;
    listeners(event: string | symbol): Function[];
    emit(event: string | symbol, ...args: any[]): boolean;
    listenerCount(type: string | symbol): number;
    // Added in Node 6...
    prependListener(event: string | symbol, listener: Function): this;
    prependOnceListener(event: string | symbol, listener: Function): this;
    eventNames(): (string | symbol)[];
}
```
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8c65c84d30d181c36ffd00c77f85181e5350ef61/node/index.d.ts#L263-L278

例えば `on` に注目すると、第一引数は `string | symbol` なら何でもよく、第二引数は `Function` なら何でもよくなっています。
とにかくゆるゆるです。
`EventEmitter` を使うときは、特定の名前の event を、特定の型の引数で発火するのが普通だと思いますが、発火する event 名や、それぞれの event に対応する引数の型を宣言する余地はこの型定義にはありません。

例えば、 [`gulp.watch` は `EventEmitter` を返します](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8c65c84d30d181c36ffd00c77f85181e5350ef61/gulp/index.d.ts#L55-L87) が、これが何という event を発火するのか、それぞれの event がどのような型の引数を渡してくるのか、型情報から読み取ることはできません。
当然、 event listener を設定する際に、 event の名前を間違えたり、 引数の型を間違えたりしても、コンパイラーが怒ってくれることもないわけです。
人が TypeScript を使うのは、まさにそうしたミスを防いでくれることを期待しているからなのにもかかわらず、外向きの interface として頻繁に利用される `EventEmitter` がこの有様であることは、これまで多くの TSer の頭を悩ませてきました（たぶん）。

## 従来の解決策

この問題を解決するために、これまでは string literal types を使った overloading が採用されることがありました。
例えば、 `fs.Writable` は以下のように定義されています。

```ts
export class Writable extends events.EventEmitter implements NodeJS.WritableStream {
    // event 定義以外は関係ないので省略
    
    /**
     * Event emitter
     * The defined events on documents including:
     *   1. close
     *   2. drain
     *   3. error
     *   4. finish
     *   5. pipe
     *   6. unpipe
     **/
    addListener(event: string, listener: Function): this;
    addListener(event: "close", listener: () => void): this;
    addListener(event: "drain", listener: () => void): this;
    addListener(event: "error", listener: (err: Error) => void): this;
    addListener(event: "finish", listener: () => void): this;
    addListener(event: "pipe", listener: (src: Readable) => void): this;
    addListener(event: "unpipe", listener: (src: Readable) => void): this;

    emit(event: string, ...args: any[]): boolean;
    emit(event: "close"): boolean;
    emit(event: "drain", chunk: Buffer | string): boolean;
    emit(event: "error", err: Error): boolean;
    emit(event: "finish"): boolean;
    emit(event: "pipe", src: Readable): boolean;
    emit(event: "unpipe", src: Readable): boolean;

    on(event: string, listener: Function): this;
    on(event: "close", listener: () => void): this;
    on(event: "drain", listener: () => void): this;
    on(event: "error", listener: (err: Error) => void): this;
    on(event: "finish", listener: () => void): this;
    on(event: "pipe", listener: (src: Readable) => void): this;
    on(event: "unpipe", listener: (src: Readable) => void): this;

    once(event: string, listener: Function): this;
    once(event: "close", listener: () => void): this;
    once(event: "drain", listener: () => void): this;
    once(event: "error", listener: (err: Error) => void): this;
    once(event: "finish", listener: () => void): this;
    once(event: "pipe", listener: (src: Readable) => void): this;
    once(event: "unpipe", listener: (src: Readable) => void): this;

    prependListener(event: string, listener: Function): this;
    prependListener(event: "close", listener: () => void): this;
    prependListener(event: "drain", listener: () => void): this;
    prependListener(event: "error", listener: (err: Error) => void): this;
    prependListener(event: "finish", listener: () => void): this;
    prependListener(event: "pipe", listener: (src: Readable) => void): this;
    prependListener(event: "unpipe", listener: (src: Readable) => void): this;

    prependOnceListener(event: string, listener: Function): this;
    prependOnceListener(event: "close", listener: () => void): this;
    prependOnceListener(event: "drain", listener: () => void): this;
    prependOnceListener(event: "error", listener: (err: Error) => void): this;
    prependOnceListener(event: "finish", listener: () => void): this;
    prependOnceListener(event: "pipe", listener: (src: Readable) => void): this;
    prependOnceListener(event: "unpipe", listener: (src: Readable) => void): this;

    removeListener(event: string, listener: Function): this;
    removeListener(event: "close", listener: () => void): this;
    removeListener(event: "drain", listener: () => void): this;
    removeListener(event: "error", listener: (err: Error) => void): this;
    removeListener(event: "finish", listener: () => void): this;
    removeListener(event: "pipe", listener: (src: Readable) => void): this;
    removeListener(event: "unpipe", listener: (src: Readable) => void): this;
}
```
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8c65c84d30d181c36ffd00c77f85181e5350ef61/node/index.d.ts#L3262-L3337

event として、 `close`, `drain`, `error`, `finish`, `pipe`, `unpipe` があることと、それぞれに決まった型の引数が紐づけられていることが分かります。
この方法の最大の欠点は、見て分かる通り、 DRY でないことです。
event 名と引数の型の紐づけはどのメソッドでも同じはずなのに、メソッドごとに繰り返し定義する必要がある上、仮にどこかのメソッドだけ間違えて定義してしまっても、コンパイラーは怒ってくれません。

重複定義しなくて済むようにするため、以前以下のような解決策を提示しましたが、これも型定義の問題を解決するためだけに EventEmitter を一枚 wrap しなければいけないという点で、あまり好ましいものではありません（今でも同様の方法を使っていますが）。
http://qiita.com/kimamula/items/82704786af4ef49630fe

## `keyof` を使った解決策

以下のように定義してしまいましょう。

```ts
export class EventEmitter<T> {
    addListener<K extends keyof T>(event: K, listener: (arg: T[K]) => any): this;
    on<K extends keyof T>(event: K, listener: (arg: T[K]) => any): this;
    once<K extends keyof T>(event: K, listener: (arg: T[K]) => any): this;
    removeListener<K extends keyof T>(event: K, listener: (arg: T[K]) => any): this;
    removeAllListeners<K extends keyof T>(event?: K): this;
    setMaxListeners(n: number): this;
    getMaxListeners(): number;
    listeners<K extends keyof T>(event: K): ((arg: T[K]) => any)[];
    emit<K extends keyof T>(event: K, arg: T[K]): boolean;
    listenerCount<K extends keyof T>(type: K): number;
    // Added in Node 6...
    prependListener<K extends keyof T>(event: K, listener: (arg: T[K]) => any): this;
    prependOnceListener<K extends keyof T>(event: K, listener: (arg: T[K]) => any): this;
    eventNames(): (string | symbol)[];
}

/**
 * event 名と引数の紐づけを interface で定義する
 */
export interface WritableEvents {
    close: void;
    drain: void;
    error: Error;
    finish: void;
    pipe: Readable;
    unpipe: Readable;
}

export class Writable extends events.EventEmitter<WritableEvents> implements NodeJS.WritableStream {
    // event 定義以外は関係ないので省略
    
    // event 定義は extends events.EventEmitter<WritableEvent> だけで OK!
}
```

Excellent!
DRY ですし、 event と引数の対応が一目で分かって可読性も高いです。

## 既存の定義との共存

`EventEmitter` に型パラメータが必須になってしまうと、けっこうな breaking change なので、現実的には以下のような定義に落ち着くのではないかと思います。

```ts
export interface EventEmitter {
    // 従来の EventEmitter 定義
}
export interface TypedEventEmitter<T> {
    // keyof を使った EventEmitter 定義
}

interface EventEmitterConstructor {
	new (): EventEmitter;
	new <T>(): TypedEventEmitter<T>;
}

declare var EventEmitter: EventEmitterConstructor;

// EventEmitter は型パラメータありでもなしでも作れる
const normalEventEmitter = new EventEmitter();
const typedEventEmitter = new EventEmitter<{ foo: string; bar: number; }>():
```

## この方法の制限

2.1 がリリースされたらすぐにでも上記の変更を DefinitelyTyped に PR してやろうかと考えていたのですが、この投稿を書きながら、この方法のひろくは許容されなさそうな制限に気づきました。
`EventEmitter` の event の引数は本来可変長ですが、この方法では、 event の引数は必ず 1 つでなければならないことです。
引数なしの event は、上記の例の `close` のように、引数を `void` で宣言することで一応定義できますが、その場合でも emit する際に `void` な引数を明示的に渡さなければならないのが切ないところです。

```ts
writable.emit('close'); // error
writable.emit('close', void 0); // ok
```

https://github.com/Microsoft/TypeScript/issues/5453 で提唱されている機能が導入されれば、このあたりうまいこと行きそうな気がするので、期待したいところです。

# 終わりに

上記のような制限はあるものの、通常の project であれば十分実用に耐えうるテクニックではないでしょうか。
個人的に、 `EventEmitter` の event に複数の引数を渡すのは可読性の点からあまり好ましくないと思っていて、そういうときは引数をオブジェクトにしてしまいますし、引数がないときに `void 0` みたいなのを書かなければならないのも、コストとして許容範囲内だと思います。

執筆時点の typescript@next で試し書きしたものを github に上げたので、よろしければご覧ください。
https://github.com/kimamula/TypeScript-definition-of-EventEmitter-with-keyof