JSON → TypeScript

JSON から TypeScript のインターフェースを生成。配列要素をマージし、任意キー・混在型の union・null の区別に対応。すべてブラウザ内で動作します。

入力
TypeScript
export interface Root {
  id: number
  name: string
  active: boolean
  address: Address
  roles: string[]
  posts: Post[]
  tags: unknown[]
}

export interface Address {
  city: string
  zip: null
}

export interface Post {
  id: number
  title: string
  views: number | string
  pinned?: boolean
}

インターフェース: 3

型はデータから推論され、推測ではない

JSON ドキュメントは自身の型を持ちません。値を持つだけで、型はそこから読み戻す必要があります。単一のオブジェクトなら簡単ですが、コレクションになると意外に微妙です。ほしい形はどれか一つのレコードの形ではなく、すべてのレコードが満たさなければならない形だからです。このツールは、サンプルが含むすべてを見て JSON サンプルから TypeScript のインターフェースを推論するので、生成される型はたまたま一番上にあった最初の行ではなく、データ全体を記述します。

変換は一方向のみです。型を JSON に戻すことは値を捏造することになり、ここでの目的はその逆です。貼り付けたドキュメントが真実の源であり、あらゆる宣言はそこから導かれます。API のレスポンス、設定ファイル、ログ行を貼り付ければ、そうでなければ手で書いていたインターフェースを読み取れます。

配列がどうマージされるか

興味深い判断はすべて配列で起こります。素朴な変換器は最初の要素だけを見て止まるため、たまたま見えたフィールドはすべて必須に見え、見えないフィールドはすべて見落とします。このツールは代わりにすべての要素を一つの型にマージし、そこから三つのことが導かれます。

  • 一部の要素にはあって他の要素にはないキーは任意になり、疑問符を付けて書かれます。レコードの半分に "middleName" があり半分にない場合、フィールドは "middleName?" であり、まさに利用側が扱わなければならないものです。
  • 要素間で値の種類が異なるキーは union になります。あるレコードでは数値、別のレコードでは文字列であるフィールドは "number | string" と型付けされます。きれいだからではなく、それがデータが実際に含むものであり、あなたのコードが受け入れなければならないものだからです。
  • 要素の内側のネストしたオブジェクトも同じように再帰的にマージされ、それ自身のインターフェースに抽出されます。それぞれ "address" を持つ十個の配列要素は、その十個すべてを記述する一つの Address インターフェースを生みます。

ドキュメント自体がトップレベルの配列である場合、ルートは要素インターフェースへのエイリアス(例えば "type Root = RootItem[]")になり、マージされた要素の型がその下に書かれます。

null と任意、そしてなぜ違うのか

null を欠けたキーと同じに扱いたくなりますが、それは誤りです。TypeScript では "name?: string" はプロパティが欠けうることを意味し、"name: string | null" は常に存在するが null を保持しうることを意味します。これらは異なる契約であり、利用側は異なる方法で確認します — "in" か値の比較かです。このツールは両者を分けます。データ中の明示的な null は union のメンバー "| null" になり、"string | null" が期待どおりに読めるよう最後に置かれます。一方、一部のレコードに単に欠けているキーは任意になります。両方であるフィールド — あるレコードでは null、別のレコードでは欠落 — は両方として、"field?: T | null" と出力されます。どちらの事実もあなたのデータについて真だからです。

ネストしたオブジェクトは名前付きインターフェースになる

ネストした形を親に埋め込む代わりに、各オブジェクトはそれが置かれているキーから名前が導かれる、それ自身のインターフェースに抽出されます。"user" オブジェクトは User インターフェースになり、その中の "address" は User が参照する Address インターフェースになります。深く埋め込まれた型は読みにくく再利用できず、名前付きインターフェースはあなた自身が書いたであろうものです。配列要素は可能なら単数化され — "users" は User を、"categories" は Category を生み — 複数形にならないキーには Item という接尾辞が付き、要素にも独自の名前が与えられます。

二つの異なるオブジェクトが同じ名前になる場合 — 例えば無関係な二つの "data" — 二つ目はマージされず接尾辞が付くので、異なる形は異なるままです。ルートオブジェクトは最初に出力され、名前を変更できます。"interface" と "type" の出力の選択はトグルです。一部のコードベースは全体を通して型エイリアスを好むからです。

unknown へのフォールバック

型情報をまったく持たない値もあります。空の配列は何でも保持しうるし、空のオブジェクトには記述すべきキーがありません。以降のすべてで型チェックを無効にする "any" に頼る代わりに、ツールは "unknown" にフォールバックします。空の配列は "unknown[]" に、空のオブジェクトは "Record<string, unknown>" になります。この違いは重要です。"any" はバグを黙って通しますが、"unknown" は利用側に使う前の値の絞り込みを強いるので、推論された型はサンプルが語ったことと語らなかったことについて正直なままです。

これらは、空のコレクションが何を保持すべきか分かった時点で手で厳密化する型です。しかしデータがそれを語るまで、"unknown" は正直な答えであり、残りの型を安全に保つものです。

何の上で、どこで動くか

すべてはあなたのブラウザ内で起こります。JSON は解析され、型はあなた自身のデバイス上で推論されます。貼り付けた内容はアップロードも保存も記録もされません。これによりツールは、本物の API レスポンスや秘密を含む設定ファイルでも安全に使えます。サンプルはページを離れません。出力は普通の TypeScript で、"d.ts" ファイルやモジュールに直接貼り付け、データが記述できなかったわずかな "unknown" フィールドを調整して使えます。

よくある質問

私の JSON はサーバーに送信されますか?
いいえ。ドキュメントは解析され、型はすべてブラウザ内で推論され、貼り付けた内容はアップロードも記録もされません。本物の API レスポンスや設定ファイルでも安全に使えます。
サンプルに存在するのに、なぜフィールドが任意になるのですか?
ツールがマージした配列の少なくとも一つの要素にそれが欠けているからです。推論は最初だけでなくすべての要素を読むので、一部のレコードが省くキーは任意になります。あなたが見たレコードがたまたまそれを含んでいても、それがデータが実際に許容する型です。
なぜフィールドが string | number のような union になったのですか?
値が配列の異なる要素で異なる種類だったからです — あるレコードでは文字列、別のレコードでは数値。マージされた型は両方を受け入れなければならないので、union として書かれます。それが意外なら、たいていはデータが予想より不均一だという意味で、知っておく価値があります。
なぜツールは any ではなく unknown を使うのですか?
記述できない値 — 空の配列、空のオブジェクト — に対して "unknown" は結果を型安全に保ち、利用側に使う前の絞り込みを強います。一方 "any" は型チェックを無効にしてしまいます。空のコレクションが何を保持するか分かれば、これらのフィールドを手で厳密化できます。
interface 出力と type 出力の違いは何ですか?
記述する型に違いはありません — どちらも同じ形を生みます。"interface" はオブジェクト型の一般的な慣用で、拡張やマージができます。"type" エイリアスは一部のコードベースが全体を通して使うことを好むものです。トグルは、出力をプロジェクトのスタイルに合わせるためにあります。
TypeScript を JSON に戻せますか?
いいえ、意図的にできません。変換は一方向で、JSON が入り、型が出ます。逆方向はあなたのデータに一度もなかった値を捏造することになり、あらゆる宣言が実際に貼り付けた内容から導かれる、という点こそが要点です。
ネストしたオブジェクトはどう名付けられますか?
それが置かれているキーからです。"user" オブジェクトは User に、その中の "address" は Address になります。配列要素は可能なら単数化され — "categories" は Category を生み — 複数形にならないキーには Item という接尾辞が付きます。名前が衝突する二つの異なる形は、マージされず接尾辞が付くので、異なるままです。