TypeScript で API レスポンスに型を付ける方法
フロントエンド開発者なら誰もが経験したことがあるでしょう。API からデータを取得し、プロパティにアクセスすると、実行時に undefined が返される — TypeScript は何も警告してくれなかったのに。問題はあなたのコードではありません。TypeScript の型システムはコンパイル時にのみ動作するため、API が実際に何を返すかを知る術がないのです。
この記事では、TypeScript で API レスポンスに型を付けるための実践的なパターンを、基本的なインターフェースの定義から実行時バリデーション、契約駆動型の型生成まで網羅します。
重要なポイント
- TypeScript の型システムはコンパイル時にのみ動作し、API が実行時に返すデータの実際の形状を検証することはできません。
- インターフェースや型エイリアスと判別可能なユニオン型を使用して、明確で期待されるレスポンス形状を定義します。
- ジェネリックな fetch ラッパーを使用すると、呼び出し側のコードがすっきりし、期待される型が明示的になります。
- 信頼できない外部データの場合は、Zod、Valibot、ArkType などの実行時バリデーションライブラリを使用して、コンパイル時の型と実際のレスポンスのギャップを埋めます。
- OpenAPI 仕様が存在する場合は、そこから型を生成してフロントエンドとバックエンドの契約を同期させます。
TypeScript が実行時に保護できない理由
response.json() を呼び出すと、TypeScript は通常、結果を unknown として扱います(環境の型定義によっては any の場合もあります)。つまり、好きな型にキャストできますが、コンパイラはそれを完全に信頼してしまいます。
// ❌ TypeScript はこのキャストを盲目的に信頼します
const data = (await response.json()) as User
console.log(data.email) // 実行時に undefined の可能性があります
これが理解すべき核心的な境界です。TypeScript で型付けされた API レスポンスはコンパイル時の安全性を提供しますが、API が返す実際の JSON を検証するわけではありません。 形状が変わっても、TypeScript は気づきません。
期待されるレスポンス形状の型定義
最初のステップは、期待する内容に構造を与えることです。インターフェースまたは型エイリアスを使用してレスポンス形状を記述します:
interface User {
id: string
name: string
email: string
}
一貫性のある API の場合、ジェネリックなラッパーを使用すると、成功状態とエラー状態の両方をきれいに処理できます:
type ApiResponse<T> =
| { status: "ok"; data: T }
| { status: "error"; message: string }
この判別可能なユニオン型パターンは、多くのオプショナルフィールドを持つ単一の型よりも透明性が高くなります。status をチェックすると、TypeScript は自動的に型を絞り込みます — 追加のガードは不要です。
ジェネリックラッパーを使用した Fetch レスポンスの型付け
TypeScript で fetch レスポンスに型を付けるには、再利用可能なヘルパーを使用するとすっきりします:
async function apiFetch<T>(url: string): Promise<T> {
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
return response.json() as Promise<T>
}
// 使用例
const user = await apiFetch<User>("/api/users/1")
これにより、呼び出し側のコードがすっきりし、期待される形状が明示的になります。同じパターンは Axios や他の HTTP クライアントでも機能します。
Discover how at OpenReplay.com.
実行時バリデーション: ギャップを埋める
as User のような型アサーションは、コンパイラへの約束であり、保証ではありません。信頼できない外部データ — サードパーティ API、ユーザー生成コンテンツ、または完全に制御できないレスポンス — には、実行時バリデーションが必要です。
Zod は最も広く使用されているオプションです。スキーマを定義し、レスポンスをパースすると、完全に型付けされた結果が得られます:
import { z } from "zod"
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
type User = z.infer<typeof UserSchema> // スキーマから派生
const data = await response.json()
const user = UserSchema.parse(data) // 形状が間違っている場合は例外をスロー
主な利点: TypeScript の型と実行時バリデーションが同じ信頼できる情報源から派生するため、同期が保たれます。
知っておくべき代替手段:
無効なデータで例外をスローしたい場合は .parse() を使用し、例外なしでエラーを適切に処理したい場合は .safeParse() を使用します。
OpenAPI を使用した契約駆動型の型付け
API に OpenAPI 仕様がある場合、openapi-typescript を使用して TypeScript の型を自動生成できます:
npx openapi-typescript ./api-spec.yaml -o ./types/api.d.ts
このアプローチにより、型を API ドキュメントと同期させることができます。実行時バリデーションと補完的な関係にあります — 生成された型はコンパイル時レイヤーを処理し、Zod や Valibot は実行時レイヤーを処理します。
適切なアプローチの選択
| シナリオ | 推奨パターン |
|---|---|
| 自分で制御する内部 API | インターフェース + 型アサーション |
| バックエンドとの共有契約 | OpenAPI 生成型 |
| サードパーティまたは信頼できない API | Zod/Valibot スキーマバリデーション |
| 両方の安全性レイヤーが必要 | 生成型 + 実行時スキーマ |
まとめ
TypeScript は構造と自動補完を提供しますが、ネットワーク経由で到着するものを検証することはできません。信頼できる API の場合、適切に定義されたインターフェースと型付けされた fetch ラッパーで十分です。外部または予測不可能なデータの場合は、型と Zod のような実行時バリデータを組み合わせます。API 仕様がある場合は、そこから型を生成します。これらのアプローチは競合するものではなく、組み合わせることで最も効果を発揮します。
よくある質問
いいえ。'as' キーワードは型アサーションであり、実行時チェックではありません。これはコンパイラに値を特定の型として扱うように指示しますが、実際のデータを検証することは何もしません。API レスポンスの形状がアサートした内容と異なる場合、TypeScript が検出できない実行時エラーが発生します。
サードパーティ API やパブリックエンドポイントなど、完全に制御できないソースからデータを消費する場合は常に、Zod または同様の実行時バリデーションライブラリを使用してください。フロントエンドとバックエンドの両方を所有しており、レスポンス形状に強い確信がある場合は、型アサーションを使用した単純なインターフェースで十分なことが多いです。
はい、この組み合わせは最大限の安全性のために推奨されます。OpenAPI 生成型はコンパイル時の自動補完と型チェックを提供し、Zod スキーマは実行時に実際のレスポンスデータを検証します。両者を組み合わせることで、型安全性スペクトラムの両方のレイヤーをカバーします。
Fetch API の response.json() メソッドは、TypeScript ではデフォルトで Promise<any> を返します。つまり、明示的にアサートまたは検証しない限り、コンパイラは結果に対して型チェックを強制しません。一部のチームは、代わりに unknown を返すより厳密な型定義でこれをオーバーライドし、明示的な検証を強制しています。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.