TypeScriptのIndex typesと Mapped typesを使ってJavaScriptライブラリをIDEで補完する

TypeScriptの型定義はアプリの初期設計時に適切な形で導入すれば後の開発が楽になりますが、使うだけならともかく自分で定義するほどには理解が及ばないというケースは多いかと思います。

特に **Index types** や  **Index types** は扱えると便利なのですが、聞かれると説明に窮することがあり、自分の理解を深めるためにもまとめました。


※以下の記事は **TypeScript3.1.1** が前提です。


## EventEmitterを型安全に扱う

ありがちですが、 **EventEmitter** を題材にします。
ほぼこちらの記事の二番煎じです。

 [TypeScript 2.1 で導入される keyof を使って EventEmitter を定義してみる](https://qiita.com/kimamula/items/ee468b1ed5b3f45dee0f "Qiita")


以下のように `interface`を定義します。

```typescript
export type EventDef = {[P in string | symbol]: any[]};
export interface ITypedEmitter<T extends EventDef> {
	addListener<P extends keyof T>(event: P, listener: (...args: T[P]) => void): this;
	emit<P extends keyof T, K extends T[P]>(event: P, ...args: K): boolean;
	eventNames<P extends keyof T>(): P[];
	listenerCount<P extends keyof T>(type: P): number;
	listeners<P extends keyof T>(event: P): Array<(...args: T[P]) => void>;
	off<P extends keyof T>(event: P, listener: (...args: T[P]) => void): this;
	on<P extends keyof T>(event: P, listener: (...args: T[P]) => void): this;
	once<P extends keyof T>(event: P, listener: (...args: T[P]) => void): this;
	removeAllListeners<P extends keyof T>(event?: P): this;
	removeListener<P extends keyof T>(event: P, listener: (...args: T[P]) => void): this;
}
```

この例ではNodeJSのEventEmitterとWeb用のEventEmitter3共通の関数を全て定義してみましたが、実業務の場合は、使う関数だけを定義すれば良いと思います。

使い方は、

```typescript
const emitter1: ITypedEmitter<{
	hoge: [string, number],
	fuga: [number, number?],
	piyo: []
}> = new EventEmitter() as any;

emitter1.on("unknown", () => { console.log("hogehoge") }); //  エラー

emitter1.on("hoge", (val1, val2) => { console.log("hogehoge") }); // ok
emitter1.emit("hoge", "hogehoge", 1); // ok
emitter1.emit("hoge", "hogehoge", "1"); // エラー

emitter1.emit("fuga", 1, 2); // ok
emitter1.emit("fuga", 1); // ok・・・2つ目の引数はオプショナル
emitter1.emit("fuga", 1, 2, 3); //  エラー
emitter1.emit("piyo", ); // ok

```

これで開発末期にスペルミスに気づいても安心です。

`as any`が気になる方も居られるかもしれませんが、元々型の無いJSライブラリを後付けで型付けする場合などは容赦無く使っていいかと思ってます。


#### tupleを使った引数指定

まず、イベント名と引数を定義する型を宣言しています。


```typescript
export type EventDef = {[P in string | symbol]: any[]};
```

キーがイベント、値が引数の配列となります。　　
以下はその定義例です。

```typescript

interface ISample extends EventDef {
	hoge: [string, number],
	fuga: [number, number?],
	piyo: []
}
```

引数の配列を`[string, number]`ように **tuple** で記述することで、第一、第二引数それぞれの型を定義することが出来ます。

また **tuple** の定義では`[number, number?]`のようにオプショナル指定も可能で、上記の場合fugaの第二引数が省略可能となります。


#### Index typesを用いた各関数へのイベントと引数の紐付け

下記の`emit`関数のように **Index types** を用いて EventDefで定義したイベント名と引数を関数に紐づけています。

```typescript
emit<P extends keyof T, K extends T[P]>(event: P, ...args: K): boolean;
```

`keyof T`は Tに含まれるプロパティ名の集合です。

```typescript
{
	hoge: [string, number],
	fuga: [number, number?],
	piyo: []
}
```

の場合は `hoge | fuga | piyo`となります。

`T[P]`は Tに含まれるプロパティPを示します。
上記例では`T["hoge"]`で`[string, number]`となります。非常に便利です。

<small>enumとはなんだったのか...</small>


`...args: K`は配列Kを可変引数に展開しますが、上記例のようにKが **tuple**で指定されていた場合でもtuple内の型を保ったまま展開されます。

`...[number, string] --->  number, string`


## 簡易ラッパーを作りIDE補完を効かせる

ここまでで型安全は保証されるためコードの保守性は問題無いですが、各関数のイベント名は文字列で入力する必要があり、コード補完が効き辛く効率化されているとは言い難いです。

そこで **Mapped types** を利用した簡易ラッパーを作成してIDEの機能を有効活用出来るようにしてみます。

 **interface** は以下のように定義します。

```typescript
export type Nullable<V> = V | null | undefined;
export type MapToNullable<T> = { [K in keyof T]: Nullable<T[K]> };
export interface IIndividualEmitter<V extends any[], P extends ( string | symbol) = any> {
	addListener(listener: (...args: V) => void): this;
	emit<U extends MapToNullable<V>>(...args: U ): boolean;
	eventName(): P;
	listenerCount(): number;
	listeners(): Array<(...args: V) => void>;
	off(listener: (...args: V) => void): this;
	on(listener: (...args: V) => void): this;
	once(listener: (...args: V) => void): this;
	removeAllListeners(): this;
	removeListener(listener: (...args: V) => void): this;
}
export interface IEventEmitterWrapper<T extends EventDef> {
	readonly originalEmitter: ITypedEmitter<T>;
	readonly events: {[P in keyof T]: IIndividualEmitter<T[P]>}
}
```

実装はこうしました。

```typescript
import {EventEmitter} from "events";

class IndividualEmitter<T extends {[P in string | symbol]: any[]}, K extends keyof T, V extends any[]> implements IIndividualEmitter<V> {
	constructor(
		private emitter: ITypedEmitter<T>,
		private name: K,
		private defaultValues: V,
	){}
	public addListener(listener: (...args: V) => void): this {
		this.emitter.addListener(this.name, listener);
		return this;
	}
	public emit<U extends MapToNullable<V>>(...args: U): boolean {
		return this.emitter.emit(this.name, ...(this.defaultValues.map((defaultValue, index) => {
			const val = args[index];
			return val != null ? val : defaultValue;
		})));
	}
	public eventName(): K {
		return this.name;
	}
	public listenerCount(): number {
		return this.emitter.listenerCount(this.name);
	}
	public listeners(): Array<(...args: V) => void> {
		return this.emitter.listeners(this.name);
	}
	public off(listener: (...args: V) => void): this {
		this.emitter.off(this.name, listener);
		return this;
	}
	public on(listener: (...args: V) => void): this {
		this.emitter.on(this.name, listener);
		return this;
	}
	public once(listener: (...args: V) => void): this {
		this.emitter.once(this.name, listener);
		return this;
	}
	public removeAllListeners(): this {
		this.emitter.removeAllListeners(this.name);
		return this;
	}
	public removeListener(listener: (...args: V) => void): this {
		this.emitter.removeListener(this.name, listener);
		return this;
	}
}
export function createEventEmitterWrapper<T extends {[P in string | symbol]: any[]}, E extends EventEmitter = any>(
	emitter: E,
	eventDef: T,
): IEventEmitterWrapper<T> {
	return Object.keys(eventDef).reduce((res: any, key) => {
		res.events[key] = new IndividualEmitter(emitter as any, key, eventDef[key]);
		return res;
	},{
		events: {},
		originalEmitter: emitter,
	});
}
```

使い方は、


```typescript

interface ISample extends EventDef {
	fuga: [number, number?],
	hoge: [string, number],
	piyo: []
}
const events = createEventEmitterWrapper<ISample>(new EventEmitter() as any, {
	fuga: [1, 2],
	hoge: ["foo", 0],
	piyo: [],
}).events;

events.hoge.on((val1, val2) => { console.log("hogehoge") }); // ok
events.hoge.emit("hogehoge", 1); // ok
events.hoge.emit("hogehoge", "1"); // エラー
events.hoge.emit(null, 1); // ok /・・・引数１はデフォルト値「foo」が設定される

events.fuga.emit(1, 2); // ok
events.fuga.emit(1); // ok・・・引数２はデフォルト値「2」が適用される
events.fuga.emit( 1, 2, 3); //  エラー
events.piyo.emit(); // ok

```

VSCodeでの補完例です。イベント名(hoge, fuga, piyo)がIDE補完で入力可能となります。

![コード補完例.gif](https://qiita-image-store.s3.amazonaws.com/0/29289/c7d919cc-aceb-11e3-ea79-6096a3675189.gif)


#### Mapped typesによる変換

以下のように`keyof`と組み合わせることで元のプロパティ名を保ったまま値のみが異なる新しいオブジェクトを定義出来ます。

```typescript
readonly events: {[P in keyof T]: IIndividualEmitter<T[P]>}
```

このように記述すれば`EventDef`で効いていた補完が`events`にも適用されます。

実際に実装する場合は（`Object.keys()`でキーを取得するための）同一名プロパティ(key)を持つObjectの実体が必要となります。


###### インスタンス生成部の実装

```typescript
export function createEventEmitterWrapper<T extends {[P in string | symbol]: any[]}, E extends EventEmitter = any>(
	emitter: E,
	eventDef: T,
): IEventEmitterWrapper<T> {
	return Object.keys(eventDef).reduce((res: any, key) => {
		res.events[key] = new IndividualEmitter(emitter as any, key, eventDef[key]);
		return res;
	},{
		events: {},
		originalEmitter: emitter,
	});
}
```

#### Mapped typesのtuple適用を利用した、引数へのnull許容化

今回のようなケースでは初期化時はkeyさえあれば良く、プロパティ値は無くとも良いですが、 

ここでは [TypeScript3.1.1](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html "TypeScript 3.1")で追加された **Mapped types**の **tuple** および **array**への適用を利用して、


初期化時のプロパティ値をデフォルト値とし、`emit`関数が呼び出された時、引数に`null`が指定された場合に補完するものとしています。

<small>※Mapped typesのarray適用を使ってみたいと思って書いただけなので、デフォルト値が要らない場合は無視してください。</small>

```typescript
emit<P extends keyof T, K extends T[P]>(...args: K): boolean;
```

この形では引数にnullが指定出来ないので

```typescript
export type Nullable<V> = V | null | undefined;
export type MapToNullable<T> = { [K in keyof T]: Nullable<T[K]> };
```

この **Mapped types** を引数の **tuple** に適用することでnull、undefinedを許容するようになります。

```typescript
emit<U extends MapToNullable<V>>(...args: U ): boolean;
```

###### emit関数補完の実装

```typescript
public emit<U extends MapToNullable<V>>(...args: U): boolean {
	return this.emitter.emit(this.name, ...(this.defaultValues.map((defaultValue, index) => {
		const val = args[index];
		return val != null ? val : defaultValue;
	})));
}
```

このように記述すれば引数にnullが指定出来るようになり、その場合にインスタンス生成時に指定されたデフォルト値が適用されるようになります。

## おわりに

慣れるまでが大変ですが、これらは手軽にオレオレフレームワークを作ってプロジェクトに適用するハードルを下げるのにも役立ちますし、型安全だけでは無く効率化ももたらします。

<small>ちなみに自分はWebStorm派です。</small>