TypeScriptでAPIレスポンスに型を付ける方法
fetchが返すデータの型はanyで、バグが生まれるまさにその場所でTypeScriptのチェックが止まってしまいます。APIのデータに本物の型を、手早く安全に付ける方法を紹介します。
TypeScriptはコードをチェックしてくれますが、実行時に届くデータまではチェックできません。await response.json()を呼んだ瞬間に得られるのはany型の値で、そこから先はコンパイラーはuser.adress.cityのようなタイプミスを含む読み取りも黙って通してしまいます。APIレスポンスに型を付ければ、この穴をふさげます。このガイドでは、サンプルから型を手早く書く方法、扱いにくい部分のモデル化、そして型だけでは足りない場面を説明します。
ステップ1:実際のサンプルから型を作る
大きなレスポンスのinterfaceを手で書くのは時間がかかり、ミスも起きやすい作業です。代わりに実際のレスポンスをコピーしましょう。たとえばブラウザーの開発者ツールの「ネットワーク」タブやAPIのドキュメントから取得し、JSONをTypeScriptに変換に貼り付けます。次のようなレスポンスは
{
"id": 1024,
"name": "Ada",
"tags": ["admin", "beta"],
"address": { "city": "London", "zip": "N1 9GU" },
"orders": [
{ "order_id": "A-17", "total": 42, "shipped_at": "2026-09-01T10:00:00Z" },
{ "order_id": "A-18", "total": 18.5, "shipped_at": null, "coupon": "WELCOME" }
]
}
ルートの型名をUserにすると、次のようになります。
export interface Address {
city: string;
zip: string;
}
export interface Order {
order_id: string;
total: number;
shipped_at: string | null;
coupon?: string;
}
export interface User {
id: number;
name: string;
tags: string[];
address: Address;
orders: Order[];
}
ジェネレーターが2件の注文から読み取った内容に注目してください。shipped_atは一方の要素でnullだったためstring | nullになり、couponは一方の要素に存在しなかったため省略可能になっています。ネストしたオブジェクトはそれぞれ名前付きのinterfaceになり、ordersの要素はOrderになりました。処理はすべてブラウザーの中で行われるので、実際の顧客データを含むレスポンスがどこかに送られることはありません。
ステップ2:fetchで型を使う
型とデータを結び付けるいちばん簡単な方法は、小さなヘルパー関数です。
async function getJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as T;
}
const user = await getJson<User>('/api/users/1024');
user.address.city; // string, checked by the compiler
as Tはコンパイラーへの約束であって、チェックではありません。サーバーが別のものを送ってきても、TypeScriptは気づきません。自分で管理している社内APIなら、このトレードオフで問題ないことも多いでしょう。それ以外のAPIについては、ステップ4まで読み進めてください。
ステップ3:扱いにくい部分をモデル化する
| JSONでは | TypeScriptでは | 理由 |
|---|---|---|
| キーが存在しないことがある | coupon?: string | 省略可能:プロパティ自体がないことがある |
| 値がnullになることがある | shipped_at: string | null | null許容:プロパティはあるが空の場合がある |
| その両方 | note?: string | null | APIが省略することも、nullを送ることもある |
| 日付 | string | JSONには日付型がない。必要な場所でnew Date()で変換する |
| 大きなID | APIが引用符付きで送るならstring | 2^53を超える数値はJavaScriptでは精度が落ちる |
| サンプル内の空の配列 | unknown[] | サンプルからは要素の形が分からない |
サンプルから分からない部分には、anyではなくunknownを使いましょう。unknownは使う前のチェックを強制しますが、anyは黙ってチェックを無効にしてしまいます。生成される型の完全さはサンプル次第なので、省略可能な項目も含むレスポンスや、さまざまな要素が複数入った配列を使ってください。
ステップ4:自分のものではないデータは実行時に検証する
サードパーティのAPIは仕様が変わりますし、プロキシはHTMLのエラーページを返しますし、古いクライアントが新しいサーバーにつながることもあります。実行時のチェックを入れておけば、こうしたケースをUIの奥深くではなく、データの入り口で分かりやすいエラーとして捕まえられます。よく使われる方法は2つです。
- JSON Schema:同じサンプルからJSON Schema生成でスキーマを作り、Ajvなどのライブラリで検証します。スキーマは言語に依存しないので、別の言語で書かれたバックエンドでも同じファイルを使えます。
- TypeScriptファーストのバリデーター:Zodのようなライブラリでは、形を一度記述すればそこからTypeScriptの型を推論できるので、型とチェックの内容が食い違うことがありません。
どちらの方法でも、検証はデータがプログラムに入ってくる場所で一度だけ行い、残りのコードは型を信頼して書けるようにします。
APIが変わったとき
一度だけ生成した型は、APIの成長とともに実態からずれていきます。生成に使ったサンプルを型と一緒にリポジトリに置いておけば、次の人が型を生成し直し、何が変わったかを差分で確認できます。APIがOpenAPIの定義を公開しているなら、サンプルよりもそちらから型を生成するほうが確実です。1つのレスポンスには決して現れないものも含め、すべての省略可能な項目と取りうる値が列挙されているからです。定義のないAPIでは、ステップ4の実行時バリデーターが、気づかれないまま起きる不一致を、ログで確認できるエラーに変えてくれます。
interfaceとtype、どちらを使う?
APIのデータには、どちらでも使えます。interfaceは拡張でき、2回宣言するとマージされるため、公開する型にはinterfaceを好むチームもあります。type(型エイリアス)は、ユニオン型やマップ型も表現できます。プロジェクトでどちらかのスタイルに決め、一貫させてください。ジェネレーターはどちらの形でも出力できます。
同じサンプルからほかの言語へ
同じJSONのサンプルから、JSON型定義ジェネレーターでGoの構造体、Pydanticモデル、C#のクラスも作れます。フロントエンドとバックエンドでデータの形をそろえておくのに役立ちます。サンプル自体が壊れている場合は、JSON整形ツールでどこが問題かを確認できます。