JSON SchemaのanyOfをTypeScriptの型で表現する

これは [ZOZO Advent Calendar 2022](https://qiita.com/advent-calendar/2022/zozo) カレンダー Vol.2 の 3日目の記事です。

## 結論

```ts
type AnyOf<T extends any[]> = T extends [infer A, ...infer B]
    ? A | AnyOf<B> | (A & AnyOf<B>)
    : never;
```

## JSON Schema とは

JSON Schema というものを聞いたことがあるでしょうか。
詳細は https://json-schema.org/ を読んでいただければと思いますが、簡単に説明すると *JSON をバリデーションする* (または *型をつける*)もので身近なところでは OpenAPI などのレスポンス型定義などに用いられます。

他にも各種設定ファイルで JSON や YAML を書くときにテキストエディタ上で補完が効くものは内部的に JSON Schema が使われていることがほとんどだと思います。

例:

```json:Userっぽい型
{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "name": { "type": "string" },
    "age": { "type": "integer" },
    "height": { "type": "number" }
  }
}
```

## JSON Schema と TypeScript

オブジェクトに型をつけているわけなので TypeScript っぽいですね。

上で例示した JSON Schema を TypeScript の型で示すと次のようになります。

```ts
type Userっぽい型 = {
  id?: string;
  name?: string;
  age?: number;
  height?: number;
};
```

`Userっぽい型['age']` を見ると、 `integer` だったものが `number` になっています。
JSON Schema はあくまでもバリデーションのためのものなので string のフォーマットが設定できたりプログラミング言語の型レベルでは難しいこともできます。

## oneOf, allOf, anyOf

JSON Schema には `oneOf`, `allOf`, `anyOf` というキーワードがあります。
この記事を読んでいる多くの方は、TypeScript を日常的に書いていると思うので言葉と一緒にTypeScriptの型の例を示します。

JSON Schema では配列で表現されるところですが、簡単のためジェネリクスで表現します。

### oneOf

```ts
type OneOf<A, B> = A | B;
```

`A` もしくは `B`

### allOf

```ts
type AllOf<A, B> = A & B;
```

`A` と `B` を含む型

### anyOf

```ts
type AnyOf<A, B> = A | B | A&B;
```

`A` と `B` 最低でもいずれかを含む型

## 本題

上の例では、AとBだけだったのでまだシンプルですが、anyOfはA,B,C,D...と増えていくととても大きくなりそうです。
実際A,B,Cの3つになると次のようになり、項の数は7個になります。($項の数=2^{文字の数}-1$ です)

```ts
type AnyOf<A, B, C> = A | B | C | A&B | A&C | B&C | A&B&C;
```

どうにか一般化できないでしょうか。
ここで一度AとBのみのパターンを `AnyOf2` とします。

```ts
type AnyOf2<A, B> = A | B | A&B;
```

このとき上のA,B,Cのパターンは次のように置き換えることができます。

```ts
// 並び替え
type AnyOf3_1<A, B, C> = A | B | C | B&C | A&B | A&C | A&B&C;
// B,Cをまとめる
type AnyOf3_2<A, B, C> = A | (B | C | B&C) | A & (B | C | B&C);
// AnyOf2をつかって
type AnyOf3_3<A, B, C> = A | AnyOf2<B, C> | A & AnyOf2<B, C>;
// さらにAnyOf2をつかって
type AnyOf3<A, B, C> = AnyOf2<A, AnyOf2<B, C>>;
```

`AnyOf(N)` は `AnyOf(N-1)` を使って表現できそうです。
さっそく `AnyOfN` を作っていきたいですが、TypeScript の generics は任意の個数の引数が取れないので配列にします。

```ts
type AnyOfN<T extends any[]> = T extends [infer First, ...infer Rest]
  ? AnyOf<[First, AnyOf<Rest>]>
  : never;
```

さて、こんな感じでしょうか。と言いたいところですがこれでは `N=2` のときの挙動がないため無限ループに陥り怒られてしまいます。

> Type instantiation is excessively deep and possibly infinite.

よくみると `?` の直後の `AnyOf` は `AnyOf2` の形をしているので書き換えます。

```ts:記事冒頭のAnyOf
type AnyOf<T extends any[]> = T extends [infer A, ...infer B]
    ? A | AnyOf<B> | (A & AnyOf<B>)
    : never;
```

完成です。

[TypeScript Playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAggdiA8gMwDwBUoQB7AnAEwGcoBDBAbQF0A+KAXikxz0JIoEs5kIAnWADRQAdKIAUXHvwBCASioAoKMqgB+WFAA+sBClTS62sTCgAyHUjQHZSlQC4ocCADc+AbgWhIURE73NcfGIySloGJgo4AFcAWwAjPioPL2gYABs0-yxAthCQajpGANZgzm4+QRFRSQrpRRU1DXN0zKsaW2UHKLgAazgAewB3OA9PcFTwgG9SOwAGAF8x72kpuLsARkWUqABhKYBjOwAmLfGoABEpgjsAZlPvAFEpiDsAFkWl6AeARyjSNNQAA0hABNQq2MQYGhiWQMOjFIIkQGNdZQBxHWEsREQqEwuFMbIlEgglFoqAYxrAXhRaAOZD-IgQBSfCwoVGMeCWVAUGC0DwAen5KgAeqoWZyUEdwhK0DyBHUaAKhcpReLdMgbtL1dyYPKBDs+QpBSKxdsZa8tVy5dJ9QJzobjSrTWcZQBWS16a2284CB4O5VQVVqyzrAD60qVJuDkvDHM00k0MFM0kjTujGtjsHjmh2ieT8dMuaTReThdTged3nNmZg2dz5zzCekhZzpgbxYL7bbjZbzZ23aTza7-a7fbb5dVZvVrprdc0DYejYLuZH89Mi47Q9bG+7zZ3O77y+7I-XedXQ9Pg93hcvN5351ve5755vD53J4Pbcfd+fX4eE7FFkdgACwgfYehldkoB+P4AUgoRINDRUjQDSczhAsCIPVKVGBg-5UBlI4EOwpCAKA0DwJlTVcN+fCqOIywblIlCo22DDKPVC0aNggjOIYlBXmYx0K3IzC3XCPC4OnfjkBnZDhKDM0MhQd0OWU2VdRtHY7V9f1WLOXwIBU8JDM9TTvV0+TULFIA)

...ところで、 `anyOf` のうまい使い所がわかりません。

---

## おまけ

oneOf, allOf, anyOf の形式を合わせると以下になります。

```ts
type OneOf<T extends any[]> = T extends [infer A, ...infer B]
    ? A | OneOf<B>
    : never;
type AllOf<T extends any[]> = T extends [infer A, ...infer B]
    ? A & AllOf<B>
    : unknown;
type AnyOf<T extends any[]> = T extends [infer A, ...infer B]
    ? A | AnyOf<B> | (A & AnyOf<B>)
    : never;
```

ちなみに oneOf は

```ts
type OneOf<T extends any[]> = T[number];
```

でも大丈夫です。