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 | nullnull許容:プロパティはあるが空の場合がある
その両方note?: string | nullAPIが省略することも、nullを送ることもある
日付stringJSONには日付型がない。必要な場所でnew Date()で変換する
大きなIDAPIが引用符付きで送るならstring2^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整形ツールでどこが問題かを確認できます。

よくある質問

JSONからTypeScriptの型を生成するには?

実際のJSONのサンプルを、JSONをTypeScriptに変換するツールに貼り付けます。すべてのオブジェクトに対してinterfaceが作られ、省略可能な項目やnullになる項目もサンプルから判定されます。

res.json()の型がanyになるのはなぜですか?

サーバーが何を送ってくるかを、TypeScriptはコンパイル時に知ることができないからです。ジェネリックなヘルパー関数やバリデーターを使って、自分で結果に型を付けます。

TypeScriptの型はデータを検証してくれますか?

いいえ。型はコンパイル時に取り除かれます。実行時にデータをチェックするには、JSON SchemaのバリデーターかZodのようなライブラリを使ってください。

省略可能(?)とnullのどちらを使うべきですか?

APIに合わせます。キーが存在しないことがあるなら疑問符を、キーはあるが値がnullになることがあるならnullとのユニオン型を使います。

JSONの日付はどう型付けすればよいですか?

JSONには日付型がないため、stringにします。日付の計算が必要な場所で、Dateオブジェクトに変換してください。