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;
}ネストされた各オブジェクトは名前付きインタ ーフェースになります。名前は親キー由来 —— address は Address に、orders の要素は Order になります。これにより型が読みやすく、組み合わせやすくなります。
型マッピング規則
JSON 型が TypeScript 型にどうマッピングされるかを理解すれば、出力が予測可能になり、自信をもって調整できます。
| JSON 値 | TypeScript 型 |
|---|---|
"hello" | string |
42, 3.14 | number |
true, false | boolean |
null | null |
{} | 名前付き interface |
['a', 'b'] | string[] |
[1, 'two'] | (number | string)[] |
[](空) | unknown[] |
細かい話: TypeScript には整数型がありません。42 も 3.14 も number になります。この区別が用途上重要なら、ブランド型を使うかコメントを追加してください。
方法 1: オンライン JSON-to-TypeScript コンバーターを使う
1 回限りのタスク —— 新しい API エンドポイントや素早いプロトタイプ —— にはオンラインツールが最速です。fixjson の JSON-to-TypeScript コンバーター はブラウザ内で変換を完結します。
- JSON を貼り付け 入力パネルに。ツールは有効な JSON、または末尾カンマやシングルクオートのような一般的な不具合がある壊れた JSON も受け取り、変換前に修復します。
- TypeScript インターフェースが即座に表示 右側に —— ボタンを押す必要はありません。
- 出力をコピーして、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 typescriptquicktype は 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); // 形状が合わなければ throwZod のアプローチは両者の良いとこ取りです。スキーマが 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 パーサーとも同じく —— 構文的に有効な入力を必要とします。