TypeScriptジェネリクスでAPIレスポンスを型安全に扱う

問題

API呼び出しのたびにasで型アサーションをしていました。

const res = await fetch('/api/users');
const data = await res.json() as User[];

これでは実際のレスポンス構造が変わっても型エラーが出ず、ランタイムでクラッシュしてしまいます。

解決方法

ジェネリクスのラッパー関数を一つ作ります。

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

使い方はこのようになります。

interface User {
  id: number;
  name: string;
  email: string;
}

// dataの型がApiResponse<User[]>として推論されます
const { data: users } = await fetchApi<User[]>('/api/users');

// dataの型がApiResponse<User>として推論されます
const { data: user } = await fetchApi<User>('/api/users/1');

ポイント

  • as型アサーションはコンパイラを欺くものです。ジェネリクスは型システムが直接推論するようにします。ランタイムの安全性に大きな差が出ます。
  • エラーレスポンスもジェネリクスで統一したい場合は、ユニオン型を使います:Promise<ApiResponse<T> | ApiError>
  • Axiosをお使いであれば、すでにaxios.get<T>()のようにジェネリクスをサポートしています。このパターンはfetch APIで同じ体験を実現するものです。