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.

7 min read

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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

API response discriminated union flow

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.

typescripttypescript
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.

typescripttypescript
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:

typescripttypescript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

How do I type a JSON response from an API?

Define a TypeScript interface or type alias that matches the JSON shape. Use it as the type parameter in your fetch wrapper or as the type argument to response.json(). The interface does not exist at runtime, so it does not validate the data, but it gives you compile-time safety on how you use the data.

Should I mark all API response fields as optional?

No. Mark as optional only the fields that the API documentation says might be absent. If you mark everything optional, you lose the compiler's ability to catch missing-property bugs. Read the API contract and reflect it accurately in your types.

How do I handle API responses that can be different shapes depending on success or failure?

Use a discriminated union. Define a success type with a status field like { ok: true; data: T } and an error type like { ok: false; error: string }. Narrow on the status field to access the correct shape in each branch.

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.