JSON から TypeScript へ API レスポンスからインターフェースを自動生成
APIレスポンスごとにTypeScriptのインターフェースを手動で書くのは面倒で、誤りが生じやすい。実際のJSONデータから正確なインターフェースを自動生成する方法を学び、Zodを使って実行時検証を追加する。なぜなら、型は実行時に消えてしまうからであり、`any`は解決策ではない。
あなたは、第三者APIからデータを取得しました。応答はネストされたオブジェクトや配列、nullableフィールドを含む濃いJSONブロブです。そして、TypeScriptでそのデータを型として定義する必要があります。そのため、新しいファイルを開き、打ち始めるのです。 interface User { ... }そして20分後、おそらく実際のデータと一致するものを作成します。おそらくです。
もっと良い方法があります。JSONを直接TypeScriptインターフェースに変換するツールが、この20分の作業を数秒に短縮します。この記事では、その生成された型の見方、不自然なケース(null、ユニオン、深層ネスト)の処理方法、そしてコンパイル時だけでなく実行時にも形状の不一致を検出するために生成された型とZodスキーマを組み合わせるべき理由について説明します。
API応答に対するTypeScriptインターフェースがなぜ重要なのか
TypeScriptの価値は、コードが実行される前にエラーを検出することです。タイプ付きAPI応答がないと、プロパティが存在しない可能性があるアクセス、オプションな値を必須として扱ったり、下流で予期しない変換が行われるようになります。 "null" 文字列を予期しないものに変換するのです。
次のような一般的なシナリオを考えてみましょう:
const user = await fetchUser(id);
console.log(user.address.city); // TypeError at runtime if address is null
もし適切な型で応答をタイプしていたら—— address: Address | null ——TypeScriptはそのアクセスをすぐに警告してくれます。コンパイラはあなたの最初の防御線ですが、それに頼るには、適切な情報を与える必要があります。
すべてのAPIに対して手動でインターフェースを書くのは面倒で、誤りを生みます。スキーマを誤解したり、オプションフィールドを漏れたり、古いバージョンをコピー&ペーストしたりします。実際のJSONデータからインターフェースを生成することで、その人間の誤りを除去できます。
JSONからTypeScriptに変換されるもの
シンプルなAPI応答を取ってみましょう:
{
"id": 42,
"username": "jsmith",
"email": "j@example.com",
"createdAt": "2024-01-15T10:30:00Z",
"role": "admin",
"profile": {
"bio": "Developer",
"avatar": null
}
}
それを JSONからTypeScriptインターフェースジェネレーター に貼り付けます。そして、次のようになります:
interface Profile {
bio: string;
avatar: null;
}
interface RootObject {
id: number;
username: string;
email: string;
createdAt: string;
role: string;
profile: Profile;
}
いくつかの注意点があります:
- ネストされたオブジェクトはそれぞれのインターフェースになります —
Profileは自動的に抽出され、インラインにされません。 - 日付は
stringとしてタイプされます。JSONには日付型がないため、ISO文字列は文字列として残ります。それらを自分でパースする必要があります。 avatar: nullはリテラルとしてタイプされますnull——これは正確ですが、不完全です。以下で詳しく説明します。
難しいケースの処理
nullableフィールド
サンプルJSONに null がある場合、生成器はそれを nullとしてタイプします。しかし実際には、そのフィールドはデータによって実際の値とnullのどちらかになります。そのため、手動で調整する必要があります:
// Generated
avatar: null;
// What you actually want
avatar: string | null;
同じことが、サンプルに存在するがオプションであるフィールドにも当てはまります——そのプロパティに ? を追加します。
配列
オブジェクトの配列は適切に処理されます。次のようになります:
{
"posts": [
{ "id": 1, "title": "Hello", "published": true },
{ "id": 2, "title": "World", "published": false }
]
}
生成器は次のようになります:
interface Post {
id: number;
title: string;
published: boolean;
}
interface RootObject {
posts: Post[];
}
ユニオン型
もしJSONサンプルが、異なるレコードで異なる型を持つフィールドを持っている場合——たとえば、 value が数値または文字列のいずれかになる場合——そのフィールドをユニオンとして表現すべきです。生成された型は、1つのサンプルではそのようなケースを検出できません。そのため、APIドキュメントと照合することが重要です:
value: string | number;
深層ネストされたオブジェクト
深層ネストは、手動タイプ化が本当に破綻する場面であり、生成器がその価値を発揮する場面です。3つまたは4つのレベルのネストを持つ応答は、名前付きインターフェースの階層に分解され、それぞれが自らの形状を担当します。
コンパイル時型と実行時現実のギャップ
TypeScriptの初心者がよく見落とすことは次の通りです: 型は実行時には消えます。 TypeScriptはJavaScriptにコンパイルされ、JavaScriptにはインターフェースという概念がありません。もしAPIが宣言された型と一致しない場合、TypeScriptはそれを知らず、それを通知しません。
これは、API応答をキャストするという一般的なパターンを実際に危険にします:
const data = await response.json() as User; // No validation, just trust
そのキャストはTypeScriptに「信頼してください、これは Userです」と伝えます——しかしTypeScriptはそれを検証する方法を持ちません。もしAPIが形状を変更したり、エラー対象を返す場合、コードは実行時に破綻し、コンパイラが警告しなかったことを意味します。
解決策は実行時検証です。
Zodによる実行時検証
Zod はTypeScriptを第一に設計されたスキーマ検証ライブラリです。一度スキーマを定義し、入力データをパースし、形状が一致する場合に完全にタイプされた値を返し、一致しない場合に詳細なエラーを返します。キャストや推測は不要です。
先ほどと同じJSONサンプルを使用し、 JSONからZodスキーマジェネレーター が生成されます:
import { z } from "zod";
const ProfileSchema = z.object({
bio: z.string(),
avatar: z.null(),
});
const RootObjectSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string(),
createdAt: z.string(),
role: z.string(),
profile: ProfileSchema,
});
type RootObject = z.infer<typeof RootObjectSchema>;
最後の行に注目してください: z.infer スキーマからTypeScript型を直接導出します。これにより、コンパイル時型安全と実行時検証を、1つのソースから得ることができます。
フェッチの境界で使用する方法は次の通りです:
const rawData = await response.json();
const user = RootObjectSchema.parse(rawData); // throws if shape is wrong
// Or use safeParse to avoid throwing:
const result = RootObjectSchema.safeParse(rawData);
if (!result.success) {
console.error(result.error.issues);
} else {
console.log(result.data.username); // fully typed
}
生成されたスキーマを、生成器が1つのサンプルから推測できないnullableケースに調整します:
avatar: z.string().nullable(), // was z.null()
bio: z.string().optional(), // if the field might be absent
インターフェースとタイプ:どちらを使うべきか?
どちらも interface と type aliasesはTypeScriptでオブジェクトの形状を表し、API応答タイプ化のためには互換性があります。実用的な違いは次の通りです:
- インターフェースは拡張およびマージ可能です ——複数のファイルでベースタイプを拡張したい場合に便利です。宣言マージにより、他の場所で定義されたインターフェースにフィールドを追加できます。
- タイプアリーズはより柔軟です ——ユニオン、インテリセクション、タプル、マップタイプを表現でき、インターフェースはできません。
- エラーメッセージはインターフェースの場合により明確です ——TypeScriptはタイプアリーズをエラーメッセージに展開し、深層ネストのエラーを読みにくくすることがあります。
API応答の形状に関しては、どちらでも問題ありません。拡張を予見する場合は interface 、ユニオンまたはインテリセクションのセマンティクスが必要な場合は type を使用します。コードベース内の一貫性がより重要です。
実用的なワークフロー
ここに、数分で終わるワークフローを紹介します:
- 実際の応答を取得します。 ブラウザの開発ツールのネットワークタブ、Postman、またはcurlを使って、実際のAPI応答をキャプチャします。サンプルがより完全であれば、生成された型もより良いです。
- TypeScriptインターフェースを生成します。 JSONを JSONからTypeScriptインターフェースジェネレーターに貼り付けます。出力結果をプロジェクトにコピーします。
- Zodスキーマを生成します。 同じJSONを JSONからZodスキーマジェネレーターに貼り付けます。それもプロジェクトにコピーします。
- nullableおよびオプションフィールドを確認します。 生成された出力で、リテラルとしてタイプされたフィールドや、存在しない可能性のあるフィールドをスキャンします。それらを
nullに更新します。string | null,.nullable()、 または.optional()必要に応じて。 - フェッチの境界で検証します。 すべての
as YourTypeキャストをYourSchema.parse()またはsafeParse()に置き換えます。これにより、型が正確に一致していることが保証されます。
これが完全なループです——原始的なJSONからコンパイル時安全と実行時保証を数分で得ることができます。
恵 スコアボードが到着しました!
スコアボード ゲームを追跡する楽しい方法です。すべてのデータはブラウザに保存されます。さらに多くの機能がまもなく登場します!
