Type-Safe API Responses with TypeScript Generics
Problem
Casting API responses with as on every fetch call:
const res = await fetch('/api/users');
const data = await res.json() as User[];
If the actual response shape changes, TypeScript won’t catch it — it blows up at runtime instead.
Solution
Create a generic fetch wrapper:
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();
}
Usage:
interface User {
id: number;
name: string;
email: string;
}
// data is typed as ApiResponse<User[]>
const { data: users } = await fetchApi<User[]>('/api/users');
// data is typed as ApiResponse<User>
const { data: user } = await fetchApi<User>('/api/users/1');
Key Points
asassertions lie to the compiler. Generics let the type system infer correctly. The runtime safety difference is significant.- For error responses, use a union type:
Promise<ApiResponse<T> | ApiError>. - If you’re using Axios, it already supports generics via
axios.get<T>(). This pattern gives the same experience with the native fetch API.