TypeScript で型に応じた型レベル条件分岐がしたい

[TypeScript Advent Calendar 2017](https://qiita.com/advent-calendar/2017/typescript) 25日目の記事です。

# はじめに

みなさんも型レベルで条件分岐したいですよね？
つい先日も [Type-level TypeScript](http://ryota-ka.hatenablog.com/entry/2017/12/21/000000) という記事が上がっていましたし、[gcanti/typelevel-ts](https://github.com/gcanti/typelevel-ts) という**ライブラリ**もあるということがすべてを物語っています。

現状ではある型パラメーター `T` が `A` という型だったらのような分岐は(そのままでは)できません。TypeScript の issue としてもそのようなコードを書きたいというものが作られていて熱いです[^mapped_conditional_types]。
ここではどのようなワークアラウンドを用いるとそのような制限を突破できるかを紹介したいと思います。

[^mapped_conditional_types]: https://github.com/Microsoft/TypeScript/issues/12424

# 内容

## やりたいこと

意味がある例を用いることで理解の助けになるとおもうので、まずはどういうことがやりたいのかを説明します。

先日、[Play と戯れていたらなぜか構文解析をしていた件について](https://qiita.com/watiko/items/af7b9f93a8f2e2f860e6) という記事を書きました。内容は、 REST API を作り、作った API のバリデーションエラーを適切な構造に変換するというもので、単純化すると以下の通りです。

```ts
// API にはこのような形式でリクエストを送る
interface CreateUserRequest {
  name: string,
  favoriteNumbers: number[],
  address: {
    country: string,
    prefecture: string
  }
}

// API からこのようなデータを受け取れる(変換済み)
interface CreateUserErrorResponse {
  name?: string,
  favoriteNumbers?: string[],
  address?: {
    country?: string,
    prefecture?: string
  }
}
```

さて、`Request` から `ValidationErrorResponse` への変換規則はそう大したものではありませんがこんなことを手で書くわけにはいきません(面倒なので)。
考えられる選択肢としては TypeScript のコードを生成する何か[^type-converter]を用意するか [Mapped Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types) のようなものを利用して TypeScript 内で完結するかです。
そして、圧倒的に後者のほうが取り回しやすそうだったので今回の内容となるわけです。

欲しいものは次の条件を満たす `ErrorResponse` になります。

- `ErrorResponse<CreateUserRequest>` と `CreateUserErrorResponse` が等価[^equality]

[^equality]: ここでは展開しきった時に同じ形になるくらいの意味です

## 動くもの

天下り式に TypeScript 2.5.x 以上で動くものを見てみましょう。(2.5.x 未満でなぜ動かないかは未調査)

```ts
// hack
declare global {
  interface Object {
    '**T**': never;
    '**kind**': 'Object';
  }
  interface Array<T> {
    '**T**': T;
    '**kind**': 'Array';
  }
  interface Number {
    '**T**': never;
    '**kind**': 'Number';
  }
  interface String {
    '**T**': never;
    '**kind**': 'String';
  }
  interface Boolean {
    '**T**': never;
    '**kind**': 'Boolean';
  }
}

// リクエストに使われる型の定義
type BaseRequestSingleType = Number | String | Boolean;

type BaseRequestArrayType = Array<any>;

type BaseRequestObjectType<T> = {
  [P in keyof T]: BaseRequestSingleType | BaseRequestArrayType | BaseRequestObjectType<T[P]>
};

type BaseRequestTypes<T> = BaseRequestSingleType | BaseRequestArrayType | BaseRequestObjectType<T>

// 条件分岐を再帰的に行う
type ErrorResponseCase<T extends BaseRequestTypes<T>> = {
  'Object': ErrorResponse<T>,
  'Array': ErrorResponseCase<T['**T**']>[],
  'Number': string,
  'String': string,
  'Boolean': string
}[T['**kind**']];

export type ErrorResponse<T extends BaseRequestObjectType<T>> = {
  [P in keyof T]?: ErrorResponseCase<T[P]>
};
```

```ts
interface CreateUserRequest {
  name: string,
  favoriteNumbers: number[],
  address: {
    country: string,
    prefecture: string
  }
}

const e: ErrorResponse<CreateUserRequest> = {
  name: '',
  favoriteNumbers: [''],
    address: {
      country: ''
    }
}
```

## 中身

[indexed access operator](https://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types) でいろいろやるのはまあいいのですが、なにやら `declare global` などという不穏な文言が見えますね :innocent:

1. `**T**`: Array<T> の T を保持しています。型レベルの演算で取り出せるとよいのですが……
2. `**kind**`: 今回取り扱っている型を見分けるために string literal type を保持しています

後者に関しては [type-level function application](https://github.com/Microsoft/TypeScript/pull/17961) などが入ればネストは増えますがハックがなくても書けるようになるはず。

`global` に何かしている部分について目をつぶればあとは割と素直なコードになっていると思います。

1. `ErrorResponse` は型パラメータとして渡されたオブジェクトのプロパティそれぞれに `ErrorResponseCase` を適用します
2. `ErrorResponseCase` は型パラメータとして渡された型のプロパティ `**kind**` を取り出し、その型に応じて動作を分けます(条件分岐)
  - `Object` なら再帰的に `ErrorResponse` を適用します
  - `Array` なら `**T**` の型をもとに `ErrorResponseCase` を適用した結果の配列を返します
  - `Number`, `String`, `Boolean` なら終端なので `string` を返します

# まとめ

冒頭で述べていたワークアラウンドとは `declare global` することだったのです :innocent:
というわけで型に応じて型レベルで条件分岐する方法は分かっていただけたと思うのでこれで終わりたいと思います。

[^type-converter]: 静的なコードジェネレーター、babel などのようにコンパイル時のものなどいろいろ考えられそう