これは ZOZO Advent Calendar 2022 カレンダー Vol.2 の 3日目の記事です。
結論
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 が使われていることがほとんどだと思います。
例:
{
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"age": { "type": "integer" },
"height": { "type": "number" }
}
}
JSON Schema と TypeScript
オブジェクトに型をつけているわけなので TypeScript っぽいですね。
上で例示した JSON Schema を TypeScript の型で示すと次のようになります。
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
type OneOf<A, B> = A | B;
A
もしくは B
allOf
type AllOf<A, B> = A & B;
A
と B
を含む型
anyOf
type AnyOf<A, B> = A | B | A&B;
A
と B
最低でもいずれかを含む型
本題
上の例では、AとBだけだったのでまだシンプルですが、anyOfはA,B,C,D...と増えていくととても大きくなりそうです。
実際A,B,Cの3つになると次のようになり、項の数は7個になります。($項の数=2^{文字の数}-1$ です)
type AnyOf<A, B, C> = A | B | C | A&B | A&C | B&C | A&B&C;
どうにか一般化できないでしょうか。
ここで一度AとBのみのパターンを AnyOf2
とします。
type AnyOf2<A, B> = A | B | A&B;
このとき上のA,B,Cのパターンは次のように置き換えることができます。
// 並び替え
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 は任意の個数の引数が取れないので配列にします。
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
の形をしているので書き換えます。
type AnyOf<T extends any[]> = T extends [infer A, ...infer B]
? A | AnyOf<B> | (A & AnyOf<B>)
: never;
完成です。
...ところで、 anyOf
のうまい使い所がわかりません。
おまけ
oneOf, allOf, anyOf の形式を合わせると以下になります。
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 は
type OneOf<T extends any[]> = T[number];
でも大丈夫です。