How to Type API Responses in TypeScript
Every frontend developer has been there: you fetch data from an API, access a property, and get undefined at runtime — even though TypeScript never complained. The problem isn’t your code. It’s that TypeScript’s type system only operates at compile time. It has no idea what your API actually returns.
This article covers practical patterns for typing API responses in TypeScript, from defining basic interfaces to runtime validation and contract-driven type generation.
Key Takeaways
- TypeScript’s type system operates only at compile time — it cannot validate the actual shape of data returned by an API at runtime.
- Use interfaces or type aliases with discriminated unions to define clear, expected response shapes.
- A generic fetch wrapper keeps call sites clean and makes expected types explicit.
- For untrusted or external data, use runtime validation libraries like Zod, Valibot, or ArkType to close the gap between compile-time types and real-world responses.
- When an OpenAPI specification exists, generate your types from it to keep frontend and backend contracts in sync.
Why TypeScript Can’t Protect You at Runtime
When you call response.json(), TypeScript typically treats the result as unknown (or sometimes any, depending on environment typings). That means you can cast it to whatever you want — and the compiler will trust you completely.
// ❌ TypeScript trusts this cast blindly
const data = (await response.json()) as User
console.log(data.email) // Could be undefined at runtime
This is the core boundary to understand: TypeScript typed API responses give you compile-time safety, but they don’t validate the actual JSON your API returns. If the shape changes, TypeScript won’t know.
Defining Types for Expected Response Shapes
The first step is giving structure to what you expect. Use interfaces or type aliases to describe the response shape:
interface User {
id: string
name: string
email: string
}
For consistent APIs, a generic wrapper handles both success and error states cleanly:
type ApiResponse<T> =
| { status: "ok"; data: T }
| { status: "error"; message: string }
This discriminated union pattern is more transparent than a single type with many optional fields. When you check status, TypeScript narrows the type automatically — no extra guards needed.
Typing Fetch Responses with a Generic Wrapper
Typing fetch responses in TypeScript is cleaner with a reusable helper:
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>
}
// Usage
const user = await apiFetch<User>("/api/users/1")
This keeps your call sites clean while making the expected shape explicit. The same pattern works with Axios or any other HTTP client.
Discover how at OpenReplay.com.
Runtime Validation: Closing the Gap
Type assertions like as User are a promise to the compiler, not a guarantee. For untrusted external data — third-party APIs, user-generated content, or any response you don’t fully control — you need runtime validation.
Zod is the most widely used option. You define a schema, parse the response, and get a fully typed result:
import { z } from "zod"
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
type User = z.infer<typeof UserSchema> // Derived from schema
const data = await response.json()
const user = UserSchema.parse(data) // Throws if shape is wrong
The key benefit: your TypeScript types and your runtime validation stay in sync because they derive from the same source of truth.
Alternatives worth knowing:
Use .parse() when you want to throw on invalid data, or .safeParse() when you want to handle errors gracefully without exceptions.
Contract-Driven Typing with OpenAPI
If your API has an OpenAPI specification, you can generate TypeScript types automatically using openapi-typescript:
npx openapi-typescript ./api-spec.yaml -o ./types/api.d.ts
This approach keeps your types in sync with your API documentation. It’s complementary to runtime validation — generated types handle the compile-time layer, while Zod or Valibot handle the runtime layer.
Choosing the Right Approach
| Scenario | Recommended Pattern |
|---|---|
| Internal API you control | Interface + type assertion |
| Shared contract with backend | OpenAPI-generated types |
| Third-party or untrusted API | Zod/Valibot schema validation |
| Both safety layers needed | Generated types + runtime schema |
Conclusion
TypeScript gives you structure and autocomplete, but it can’t validate what arrives over the network. For APIs you trust, a well-defined interface and a typed fetch wrapper are enough. For external or unpredictable data, pair your types with a runtime validator like Zod. When you have an API spec, generate your types from it. These approaches aren’t competing — they work best together.
FAQs
No. The 'as' keyword is a type assertion, not a runtime check. It tells the compiler to treat a value as a specific type, but it does nothing to verify the actual data. If the API response shape differs from what you asserted, you will get runtime errors that TypeScript cannot catch.
Use Zod or a similar runtime validation library whenever you consume data from a source you do not fully control, such as third-party APIs or public endpoints. If you own both the frontend and backend and have strong confidence in the response shape, a simple interface with a type assertion is often sufficient.
Yes, and this combination is recommended for maximum safety. OpenAPI-generated types give you compile-time autocomplete and type checking, while Zod schemas validate the actual response data at runtime. Together, they cover both layers of the type safety spectrum.
The Fetch API's response.json() method returns Promise of any by default in TypeScript. This means the compiler will not enforce any type checks on the result unless you explicitly assert or validate it. Some teams override this with stricter typings that return unknown instead, forcing explicit validation.
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.