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で同じ体験を実現するものです。