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

  • as assertions 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.