How to Type Axios Response Data in TypeScript: A Complete Guide

Use a generic AxiosResponse<T> interface and a typed request helper to let TypeScript infer the exact shape of your JSON API responses, eliminating any types and catching contract mismatches at compile time.

When consuming JSON APIs with axios in TypeScript, properly typing the response data prevents runtime errors and unlocks IDE autocompletion. The microsoft/TypeScript repository demonstrates this pattern through its own compiler test suite, showing how generic type inference propagates through promise-based HTTP clients. This guide explains how to type axios response typescript patterns using the same mechanisms that power the TypeScript compiler itself.

Why Generic Type Inference Matters for Axios

TypeScript’s type system can propagate generic arguments through promise-based APIs. In the microsoft/TypeScript test suite, you can see this pattern in tests/cases/compiler/destructureOfVariableSameAsShorthand.ts:

interface AxiosResponse<T = never> {
    data: T;
}

declare function get<T = never, R = AxiosResponse<T>>(): Promise<R>;

The get declaration is generic over T (the payload) and returns a Promise of AxiosResponse<T>. The compiler automatically infers T from the call site. This is the same mechanism you can employ with axios.

The core of this inference lives in the compiler’s type-checker (see src/compiler/checker.ts), where generic signatures are instantiated and the resulting type is substituted throughout the call expression. When you use a default type parameter of T = never, TypeScript raises an error if the payload type isn’t supplied or inferred, preventing accidental any leakage.

Step-by-Step: Typing Axios JSON Responses

1. Define Your API Payload Interfaces

Start by describing the JSON payload with explicit interfaces. This creates the contract between your application and the API.

// src/api/types.ts
export interface User {
    id: number;
    name: string;
    email: string;
}

export interface Post {
    id: number;
    title: string;
    body: string;
    userId: number;
}

2. Create a Generic Response Wrapper

Mirror the axios response shape with a generic interface that defaults to never. This forces consumers to specify a type or rely on inference.

// src/api/axiosResponse.ts
export interface AxiosResponse<T = never> {
    data: T;
    status: number;
    statusText: string;
    headers: Record<string, string>;
}

3. Build a Typed Request Helper

Write a wrapper function that forwards the generic type argument to axios. This centralizes configuration and ensures the return type propagates correctly.

// src/api/client.ts
import axios, { AxiosRequestConfig } from "axios";
import { AxiosResponse } from "./axiosResponse";

/**
 * Generic GET request – infers the response payload type.
 *
 * @param url The endpoint URL
 * @param config Optional axios config
 * @returns Promise resolving to a typed AxiosResponse<T>
 */
export async function get<T = never>(
    url: string,
    config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
    const raw = await axios.get<T>(url, config);
    // Map axios's own response shape to our wrapper
    return {
        data: raw.data,
        status: raw.status,
        statusText: raw.statusText,
        headers: raw.headers,
    };
}

Notice the <T> on axios.get<T>—this tells axios what shape to expect for raw.data. The wrapper then returns a consistent AxiosResponse<T> that you control across the codebase.

4. Consume with Full Type Safety

When you call the helper, TypeScript infers the concrete type from your generic argument, giving you a strongly typed response.data.

// src/example/usage.ts
import { get } from "../api/client";
import { User, Post } from "../api/types";

async function fetchUser() {
    // T is explicitly set to User
    const response = await get<User>("/api/users/42");
    // response.data is strongly typed as User
    console.log(`Name: ${response.data.name}`);
}

async function fetchPosts() {
    // T is inferred as Post[] from the generic argument
    const { data: posts } = await get<Post[]>("/api/posts");
    posts.forEach(p => console.log(p.title));
}

What you gain:

  • response.data is never any.
  • IDE autocompletion and refactoring work flawlessly.
  • Compile-time errors catch mismatched fields (e.g., missing email on User).

Handling Dynamic and Union Types

If an endpoint returns multiple possible shapes, preserve safety using discriminated unions:

type SearchResult = User | Post;

async function search(query: string) {
    const { data } = await get<SearchResult[]>("/api/search", { params: { q: query } });
    data.forEach(item => {
        if ("email" in item) {
            // TypeScript narrows to User
            console.log(item.email);
        } else {
            // Narrowed to Post
            console.log(item.title);
        }
    });
}

Summary

  • Define interfaces that mirror your JSON API payloads to create compile-time contracts.
  • Use a generic AxiosResponse<T> with a default of never to prevent untyped data leakage.
  • Wrap axios calls in a typed helper function that forwards the generic <T> to axios.get<T>, ensuring the compiler propagates types through the promise chain.
  • Leverage type inference at the call site so response.data is automatically typed without manual annotation on every request.
  • Apply discriminated unions for endpoints that return heterogeneous data, maintaining type safety through narrowing.

Frequently Asked Questions

How do I type axios response data without using generics?

You can explicitly cast the response using type assertions (e.g., response.data as User), but this bypasses compile-time safety and shifts errors to runtime. The generic approach shown in tests/cases/compiler/destructureOfVariableSameAsShorthand.ts is preferred because it uses TypeScript’s inference engine to validate the shape at compile time.

What is the difference between AxiosResponse and a custom response interface?

The built-in AxiosResponse<T> from the axios package includes additional properties like config and request that may expose internal axios details. A custom interface (as demonstrated in src/api/axiosResponse.ts) lets you control exactly which fields are exposed to your application, ensuring a stable contract even if axios internals change.

How can I handle errors with typed axios responses in TypeScript?

Axios errors are typically typed as AxiosError, which accepts a generic for the error response body. You can narrow error types using instanceof AxiosError checks, then access error.response?.data with the appropriate type. This maintains type safety in both success and failure paths without reverting to any.

Does TypeScript infer axios response types automatically?

No, TypeScript cannot infer the specific JSON structure of an external API without explicit type annotations or generics. As shown in the microsoft/TypeScript source analysis, you must provide a generic argument (e.g., axios.get<User>) or a wrapper function that propagates the generic type parameter, allowing the compiler to substitute the concrete type throughout the call chain.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →