8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ZOZOAdvent Calendar 2022

Day 3

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

Last updated at Posted at 2022-12-02

これは 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 が使われていることがほとんどだと思います。

例:

Userっぽい型
{
  "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;

AB を含む型

anyOf

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

AB 最低でもいずれかを含む型

本題

上の例では、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.

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

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

完成です。

TypeScript Playground

...ところで、 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];

でも大丈夫です。

8
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?