Type API Response Data in TypeScript
Learn to model API response types in TypeScript. Covers JSON response shapes, optional fields, discriminated unions for states, and narrowing unknown data from external APIs.
When you call an API, the response comes back as raw JSON with no type information. TypeScript cannot know what shape that JSON has unless you tell it. Typing API response data means defining interfaces that describe the expected shape, then using those types everywhere you handle the data.
The types disappear at runtime, so they do not validate the data. But they give you autocomplete, refactoring, and compile-time checks on every property you access.
Defining Response Types
Start by reading the API's response shape and writing a TypeScript interface for it. Consider an endpoint that returns a user object with an id, name, email, and creation date.
interface UserResponse {
id: number;
name: string;
email: string;
createdAt: string;
}Use this interface when you fetch the data. Every property access on the returned object is now checked by the compiler.
async function getUser(id: number): Promise<UserResponse> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return response.json() as UserResponse;
}Callers get full type safety on the response. Accessing user.name gives a string. Accessing a property that does not exist in the interface produces a compiler error immediately.
Handling Optional and Nullable Fields
APIs often omit fields or return null for missing values. Reflect this in your types with the optional modifier for fields that may be absent and union with null for fields that are always present but may have a null value.
interface UserResponse {
id: number;
name: string;
email: string;
bio?: string;
avatarUrl: string | null;
createdAt: string;
}The difference matters. An optional field like bio may be absent from the object entirely. A nullable field like avatarUrl is always present on the object but its value can be null. TypeScript treats these differently: optional fields require checking for undefined before use, while nullable fields require checking for null.
When you access bio, TypeScript warns that the property might be undefined. Use optional chaining or an if check to narrow it. When you access avatarUrl, TypeScript warns that the value might be null. Use a null check before calling string methods on it.
Typing Paginated Responses
Many APIs wrap list results in a pagination envelope instead of returning a bare array. This envelope typically includes the data array plus metadata like total count, current page, and page size. Define a generic pagination type once and reuse it across all your list endpoints.
interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}Use it with any entity type. The generic T makes PaginatedResponse reusable for users, posts, orders, or any list endpoint. Callers get type-safe access to both the pagination metadata and the array of items without writing duplicate wrapper interfaces.
Using Discriminated Unions for Response States
Some endpoints return completely different shapes depending on the outcome. Model this with a discriminated union that uses a status field to distinguish the cases.
type ApiResponse<T> =
| { status: "ok"; data: T }
| { status: "error"; message: string; code: number };When you narrow on the status field, TypeScript knows exactly which shape you are working with in each branch of the conditional. The compiler tracks the discriminant and narrows the union automatically.
const result: ApiResponse<UserResponse> = await fetchUser(1);
if (result.status === "ok") {
console.log(result.data.name);
} else {
console.error(result.message, result.code);
}The compiler narrows the result type based on the status check. Inside the if branch, result.data is accessible. Inside the else branch, result.message and result.code are available instead. You cannot mix them.
The diagram shows the two possible states. The compiler uses the status field to determine which branch you are in. This pattern is especially useful for endpoints where success and failure responses carry completely different data shapes.
Typing Nested and Complex Shapes
Real APIs return nested objects and arrays. Compose smaller interfaces to model the full shape without duplication.
interface PostResponse {
id: number;
title: string;
body: string;
author: UserResponse;
tags: string[];
comments: CommentResponse[];
}
interface CommentResponse {
id: number;
body: string;
author: Pick<UserResponse, "id" | "name">;
}Use Pick and Omit to reuse parts of existing types. The nested types chain together so that accessing post.author.name is fully typed all the way down. You do not need to redefine the author shape for every entity.
When to Add Runtime Validation
TypeScript types are compile-time only. If the API changes its response shape without notice, your types will be wrong and runtime errors will occur. For critical data, add a validation step with a type guard.
function isUserResponse(data: unknown): data is UserResponse {
return (
typeof data === "object" &&
data !== null &&
"id" in data &&
"name" in data &&
"email" in data
);
}The return type data is UserResponse makes this function a type guard. When you call it inside an if condition, TypeScript narrows the checked value from unknown to UserResponse. This is the runtime check that confirms the JSON actually matches your interface.
Now use the type guard when fetching data:
async function getValidatedUser(id: number): Promise<UserResponse> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
if (!isUserResponse(data)) {
throw new Error("Invalid user response shape");
}
return data;
}If the API returns a shape that does not match, the function throws before the caller ever touches invalid data. This pairs compile-time types with runtime safety.
For more on validation, see runtime validation in TypeScript and use Zod with TypeScript.
Common Mistakes
Marking everything optional. If the API always returns id, do not mark it as optional. Overusing optional fields hides real bugs because TypeScript cannot tell you when a required field is missing.
Treating type assertions as validation. Writing as UserResponse does not check anything at runtime. If the API returns an id as a string instead of a number, your code will crash on numeric operations. Types describe what you expect; validation confirms it.
Confusing optional and nullable fields. An optional field may not exist on the object. A nullable field always exists but holds null. They require different handling, and confusing them leads to unnecessary optional chaining or missed null checks.
For building the client that makes these typed calls, see build a typed fetch wrapper.
Rune AI
Key Insights
- Define interfaces or type aliases that match your API's JSON response shapes.
- Mark fields optional only when the API genuinely omits them, not as a safety net.
- Use discriminated unions for endpoints that return different success and error shapes.
- Types are compile-time only. Pair them with runtime validation for external data.
- Narrow unknown data with type guards before accessing nested properties.
Frequently Asked Questions
How do I type a JSON response from an API?
Should I mark all API response fields as optional?
How do I handle API responses that can be different shapes depending on success or failure?
Conclusion
Typing API response data means defining interfaces that match the JSON shapes your endpoints return. Mark fields as optional only when the API can omit them. Use discriminated unions for endpoints that return different shapes for success and failure. And remember that types are compile-time only: if you need runtime validation, pair your types with a validation step. The types give you editor support and compile-time safety; validation gives you runtime confidence.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.