← 記事一覧

JSON から TypeScript インターフェースを生成する方法

JSON を TypeScript インターフェースに変換する方法 —— 手動、オンラインツール、コードで。ネストオブジェクト、オプショナルフィールド、配列、nullable 型、API と型を同期させる方法をカバー。

TypeScript 開発者なら誰もが経験する状況です。新しい API を組み込もうとして、サンプルレスポンスは手元にあるが、ビジネスロジックを 1 行書く前に型付きのインターフェースが必要になる。これらのインターフェースを手で書くのは退屈で、繰り返しが多く、ミスも起きやすい —— 特にレスポンスが深くネストされていたり、フィールドが大量にあるときはそうです。この記事では、JSON を TypeScript インターフェースに変換するための実用的なアプローチをすべて取り上げます。出力がどうあるべきか、どう自動生成するか、そして厄介なケースの扱い方を説明します。

JSON データに TypeScript インターフェースが重要な理由

型注釈なしで TypeScript で API を呼び出すと、パースされたレスポンスは any になります。つまり、自動補完なし、型チェックなし、コンパイル時の安全性なし。プロパティ名のタイプミスはビルドエラーではなくランタイムバグになります。

// 型なし —— すべて any
const response = await fetch('/api/users/42');
const user = await response.json(); // user: any
console.log(user.naem); // タイプミス —— コンパイル時にエラーにならない

// 型付きインターフェースあり
interface User {
  id: number;
  name: string;
  email: string;
}

const response = await fetch('/api/users/42');
const user: User = await response.json();
console.log(user.naem); // Error: Property 'naem' does not exist on type 'User'

インターフェースはランタイムコストがゼロ —— コンパイル時にのみ存在します。しかし、開発体験は一変します。

JSON から TypeScript への変換結果

すべての JSON オブジェクトは TypeScript の interface になり、すべての JSON プリミティブは対応する TypeScript 型にマッピングされ、配列は要素型を推論します。

// 入力 JSON
{
  "id": 42,
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "active": true,
  "score": 98.5,
  "address": {
    "street": "123 Main St",
    "city": "New York",
    "zip": "10001"
  },
  "tags": ["admin", "editor"],
  "orders": [
    { "id": 101, "total": 49.99, "status": "shipped" },
    { "id": 102, "total": 99.00, "status": "pending" }
  ]
}
// 出力 TypeScript
interface Root {
  id: number;
  name: string;
  email: string;
  active: boolean;
  score: number;
  address: Address;
  tags: string[];
  orders: Order[];
}

interface Address {
  street: string;
  city: string;
  zip: string;
}

interface Order {
  id: number;
  total: number;
  status: string;
}

ネストされた各オブジェクトは名前付きインターフェースになります。名前は親キー由来 —— addressAddress に、orders の要素は Order になります。これにより型が読みやすく、組み合わせやすくなります。

型マッピング規則

JSON 型が TypeScript 型にどうマッピングされるかを理解すれば、出力が予測可能になり、自信をもって調整できます。

JSON 値TypeScript 型
"hello"string
42, 3.14number
true, falseboolean
nullnull
{}名前付き interface
['a', 'b']string[]
[1, 'two'](number | string)[]
[](空)unknown[]

細かい話: TypeScript には整数型がありません。423.14number になります。この区別が用途上重要なら、ブランド型を使うかコメントを追加してください。

方法 1: オンライン JSON-to-TypeScript コンバーターを使う

1 回限りのタスク —— 新しい API エンドポイントや素早いプロトタイプ —— にはオンラインツールが最速です。fixjson の JSON-to-TypeScript コンバーター はブラウザ内で変換を完結します。

  1. JSON を貼り付け 入力パネルに。ツールは有効な JSON、または末尾カンマやシングルクオートのような一般的な不具合がある壊れた JSON も受け取り、変換前に修復します。
  2. TypeScript インターフェースが即座に表示 右側に —— ボタンを押す必要はありません。
  3. 出力をコピーして、TypeScript ファイルに直接貼り付けます。

すべてローカルで動作します。データはどのサーバーにも送信されません —— 扱う JSON に API キー、ユーザーデータ、内部構成が含まれる場合に重要です。

方法 2: TypeScript で変換を書く

ビルドスクリプトやコードジェネレーターの一部としてプログラム的に型を生成する必要がある場合は、ロジックを自分で書けます。以下は最小限ながら完全な実装例です。

function jsonToTypeScript(value: unknown, rootName = 'Root'): string {
  const interfaces: string[] = [];
  const seen = new Set<string>();

  function cap(s: string) {
    return s ? s.charAt(0).toUpperCase() + s.slice(1) : 'Unknown';
  }

  function singular(name: string) {
    if (/ies$/i.test(name)) return name.slice(0, -3) + 'y';
    if (/[^s]s$/i.test(name)) return name.slice(0, -1);
    return name + 'Item';
  }

  function getType(val: unknown, name: string): string {
    if (val === null) return 'null';
    if (typeof val === 'boolean') return 'boolean';
    if (typeof val === 'number') return 'number';
    if (typeof val === 'string') return 'string';
    if (Array.isArray(val)) {
      if (val.length === 0) return 'unknown[]';
      const objs = val.filter(
        (v): v is Record<string, unknown> =>
          v !== null && typeof v === 'object' && !Array.isArray(v),
      );
      if (objs.length > 0) {
        const itemName = cap(singular(name));
        buildInterface(itemName, objs);
        return `${itemName}[]`;
      }
      const types = [...new Set(val.map((v) => getType(v, name)))];
      return types.length === 1 ? `${types[0]}[]` : `(${types.join(' | ')})[]`;
    }
    if (typeof val === 'object') {
      const iName = cap(name);
      buildInterface(iName, [val as Record<string, unknown>]);
      return iName;
    }
    return 'unknown';
  }

  function buildInterface(name: string, objs: Record<string, unknown>[]): void {
    if (seen.has(name)) return;
    seen.add(name);
    const allKeys = [...new Set(objs.flatMap((o) => Object.keys(o)))];
    const props: string[] = [];
    for (const key of allKeys) {
      const present = objs.filter((o) => key in o);
      const optional = present.length < objs.length ? '?' : '';
      const propType = getType(present[0][key], cap(key));
      const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `"${key}"`;
      props.push(`  ${safeKey}${optional}: ${propType};`);
    }
    interfaces.push(`interface ${name} {\n${props.join('\n')}\n}`);
  }

  const rootType = getType(value, rootName);
  const defs = interfaces.reverse().join('\n\n');
  if (rootType !== cap(rootName)) {
    return `type ${cap(rootName)} = ${rootType};\n\n${defs}`.trim();
  }
  return defs;
}

Node.js ビルドスクリプトでの使用例:

import * as fs from 'fs';

const json = JSON.parse(fs.readFileSync('api-response.json', 'utf8'));
const typescript = jsonToTypeScript(json, 'UserResponse');
fs.writeFileSync('types/user.ts', typescript);

方法 3: quicktype (CLI とライブラリ)

quicktype は JSON から TypeScript の型を生成するもっとも機能が充実したオープンソースツールです。単純なジェネレーターが見落とすエッジケースを処理し、複数の出力形式をサポートしています。

npm install -g quicktype

# ローカルファイルから
quicktype api-response.json -o types/user.ts --lang typescript

# URL から
quicktype https://api.example.com/users/42 -o types/user.ts --lang typescript

# 標準入力から
cat api-response.json | quicktype --lang typescript

quicktype は io-ts や zod のようなライブラリを使ったランタイム検証関数も生成できます —— 静的に型を付けるだけでなく、入力データを検証したい場合に便利です。

厄介なケースの扱い

nullable フィールド

JSON API はしばしば欠損値に null を返します。ジェネレーターは null を見て型として null を生成しますが、実際には string | null が欲しいことが多いはずです。

// 生成結果(初回 —— 狭すぎ)
interface Root {
  name: string;
  middleName: null;
}

// 書き直すべき形
interface Root {
  name: string;
  middleName: string | null;
}

生成後、nullable フィールドは必ず手動で確認してください。複数のサンプルレスポンスでフィールドが時に string、時に null となる場合、優れたジェネレーターは string | null を自動で推論します。

オプション vs nullable

TypeScript は field?: string(キーが存在しないことがある)と field: string | null(キーは常に存在し値が null になりうる)を区別します。

interface Order {
  id: number;
  total: number;
  discount?: number;   // 一部のレスポンスでこのキーが欠落
  note: string | null; // キーは常に存在、値は null かもしれない
}

複数の JSON サンプルを解析するジェネレーターはこの区別を正確に推論できます。単一サンプルのジェネレーターは欠落キーをオプションとしてマークしますが、キーが欠落しているのか値が null なのかは区別できません。

有効な識別子ではないキー

JSON のキーには TypeScript のプロパティ名として無効な文字が含まれることがあります。

// JSON
{ "content-type": "application/json", "x-request-id": "abc123" }

// 生成された TypeScript —— 有効だがブラケット記法が必要
interface Root {
  "content-type": string;
  "x-request-id": string;
}

// ブラケット記法でアクセス
obj["content-type"]

API を自分で管理しているなら、これを完全に避けるためにキャメルケースを推奨します。そうでない場合、引用符付きプロパティ構文は有効な TypeScript で正しく動作します。

混合型の配列

// JSON
{ "values": [1, "two", true, null] }

// 生成結果
interface Root {
  values: (number | string | boolean | null)[];
}

このようなユニオン型は技術的には正しいですが、たいてい API の設計問題の兆候です。これを見たら、フィールドが本当に混合型なのか、サンプルデータが誤解を招いているだけなのかを確認してください。

生成型を安全に使う

インターフェースの生成は最初の一歩にすぎません。インターフェースはコンパイル時の契約 —— ランタイムでは何も検証しません。response.json()any を返し、TypeScript は実際のチェックなしであなたのインターフェースにキャストさせてくれます。

両側を管理する内部 API ならこれで十分なことが多いです。外部 API やユーザー提供データの場合はランタイムで検証してください。推奨は Zod です。

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  active: z.boolean(),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string(),
  }),
  tags: z.array(z.string()),
  orders: z.array(z.object({
    id: z.number(),
    total: z.number(),
    status: z.string(),
  })),
});

type User = z.infer<typeof UserSchema>; // スキーマから派生した TypeScript 型

const raw = await response.json();
const user = UserSchema.parse(raw); // 形状が合わなければ throw

Zod のアプローチは両者の良いとこ取りです。スキーマが TypeScript 型とランタイム検証の唯一の真実源になります。API が破壊的に変わっても、データがシステムに入る地点で気づけます。

インターフェースを超えて: バリデーターと型付きクライアント

生成されたインターフェースはコンパイル時契約です。一段上の方法は、型とランタイムチェックを単一ソースから提供するライブラリを使うことです。

  • Zod io-ts —— スキーマを定義し、そこから TypeScript 型を推論し、境界で入力データを検証します。API が変わるとビルド(およびエラーログ)がすぐに知らせます。
  • tRPC 型付き fetch ラッパー(例: openapi-fetch)—— サーバーのプロシージャか OpenAPI 定義が真実源で、クライアントは対応する型を自動で取得し、間に JSON-to-TS のステップは入りません。
  • ts-pattern と判別可能ユニオン —— データにタグフィールド(例えば { kind: "circle" | "square", ... })があれば、網羅的なパターンマッチでケース漏れをコンパイル時に検出できます。

型を API と同期させる

生成された型には賞味期限があります。API は変わります —— フィールド名が変わったり、新しい必須フィールドが現れたり、string フィールドが nullable になったり。インターフェースは音もなく古くなります。

サンプルレスポンスではなく OpenAPI 仕様から生成する

API に OpenAPI (Swagger) 定義があれば、openapi-typescript が仕様から直接型を生成します。

npm install -g openapi-typescript
openapi-typescript https://api.example.com/openapi.json -o types/api.ts

これはサンプルレスポンスからの生成よりはるかに信頼でき、仕様はオプション、nullable バリエーション、列挙値を含むすべてのフィールドを記述するからです。

CI で型を再生成する

CI パイプラインに、仕様または既知の正しいサンプルから型を再生成し、その後 tsc --noEmit で破損を確認するステップを追加しましょう。API が変わればビルドが落ち、破壊的変更が本番に到達する前に気づけます。

よくある質問

JSON を TypeScript インターフェースに変換するには?

即時の出力には fixjson の JSON-to-TypeScript ツール のようなオンラインコンバーターに貼り付け、ビルドステップでは quicktype を実行し、長期維持コードには openapi-typescript で OpenAPI 仕様から生成します。

JSON 型はどう TypeScript 型にマッピングされる?

文字列 → string、すべての数値 → number(TypeScript に整数型なし)、boolean → boolean、オブジェクト → 名前付きインターフェース、配列は要素型を推論。上のマッピング表と JSON の 6 つのデータ型 を参照してください。

nullable や optional フィールドはどう扱う?

キーが存在しないことがある場合は field?: T、キーが常に存在し値が null になりうる場合は field: T | null を使います。複数サンプルを解析するジェネレーターはこれを推論しますが、単一サンプルのツールは手動レビューが必要です。

TypeScript インターフェースはランタイムで JSON を検証する?

いいえ —— インターフェースはコンパイル時に消去されます。外部データのランタイム安全性には Zod で検証するか、JSON Schema で形状を記述してください。

結論

JSON から TypeScript インターフェースへの変換は、すべての TypeScript 開発者が定期的に行うものです。正しいアプローチは状況次第です。手早い 1 回限りの統合には オンラインコンバーター で 1 分以内に始められます。ビルドパイプラインには quicktype やカスタムスクリプトでプロセスを自動化。長期の本番コードには、OpenAPI 仕様から型を生成し、ランタイムは Zod で検証する組み合わせが最強の保証を与えます。

どの方法を使うにせよ、生成されたインターフェースは出発点です —— nullable フィールドをレビューし、自明でないプロパティにはコメントを追加し、業務ルールが JSON サンプルの示唆より厳しいところは制約を引き締めてください。データを正確に反映した型は、コードベース全体の保守を容易にします。

変換しようとする前に JSON 自体に構文エラーがあるなら、まず修正してください。TypeScript ジェネレーターは —— ほかのどんな JSON パーサーとも同じく —— 構文的に有効な入力を必要とします。