【TypeScript × Axios】型安全なHTTPクライアント完全ガイド|レスポンス型・AxiosError・インターセプター・実務パターンを徹底解説

【TypeScript × Axios】型安全なHTTPクライアント完全ガイド|レスポンス型・AxiosError・インターセプター・実務パターンを徹底解説 TypeScript

TypeScriptでAPIリクエストを実装するとき、「レスポンスの型がanyになってしまう」「エラーオブジェクトの型が分からない」と感じたことはありませんか。

AxiosはTypeScriptの型定義が充実しているHTTPクライアントで、正しく使えばAPIの入出力を型安全に扱えます。本記事ではAxiosをTypeScriptで使い倒すための知識を体系的に解説します。

この記事でわかること

  • Axiosのインストールとtsconfig.jsonの設定
  • AxiosResponse<T>でレスポンスデータを型安全に取得する方法
  • AxiosErrorを使った型安全なエラーハンドリング
  • インターセプターに型を付ける方法
  • カスタムインスタンスと型安全なAPIクライアントクラスの設計
  • 実務でよく遭遇する型エラーと対処法

Axiosを初めて触る方も、すでに使っているが型定義に自信がない方も、この記事を読めばAxiosをTypeScriptで自信を持って扱えるようになります。

なお、async/awaitの基礎についてはTypeScript 非同期処理の型定義 完全ガイドをあわせてご覧ください。

スポンサーリンク

Axiosとは・なぜTypeScriptと相性が良いのか

AxiosはブラウザとNode.jsの両方で動作するHTTPクライアントライブラリです。XMLHttpRequestやfetch APIをラップし、リクエスト/レスポンスのインターセプト、自動JSONシリアライズ、タイムアウト設定など実務で必要な機能を標準で備えています。

特徴 内容
TypeScript-first 公式パッケージに型定義が同梱。追加の@typesインストールが不要
ジェネリクス対応 axios.get<T>(url)でレスポンス型を明示できる
AxiosError型 エラーオブジェクトの型が定義されており、型安全なエラー処理が書ける
インターセプター リクエスト/レスポンスの前後処理を型安全にフック可能
ブラウザ・Node.js両対応 フロントエンド・バックエンドを問わず同じAPIで使える
fetch APIとの違い
ブラウザ標準のfetchも型定義はありますが、エラー処理・インターセプター・タイムアウトを自前実装する必要があります。Axiosはこれらをすぐに使える形で提供しており、実務規模のアプリケーションでは開発コストを大幅に削減できます

インストールと初期設定

Axiosはv1.0以降、型定義が本体に同梱されています。@types/axiosは不要です。

ターミナル
# npm
npm install axios

# yarn
yarn add axios

# pnpm
pnpm add axios

インストール後、tsconfig.jsonに以下の設定が入っているか確認してください。

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node"
  }
}
esModuleInterop: trueが必須
esModuleInteropfalseの場合、import axios from "axios"がコンパイルエラーになります。import * as axios from "axios"と書く方法もありますが、esModuleInterop: trueにしておくほうがコードがシンプルになります。tsconfig.jsonの設定方法も参考にしてください。

基本的なリクエストと型定義

Axiosの各メソッドはジェネリクスを受け取り、AxiosResponse<T>を返します。Tにレスポンスデータの型を渡すことで型安全なアクセスができます。

GETリクエスト

TypeScript
import axios from "axios";

// レスポンスデータの型を定義
interface User {
  id: number;
  name: string;
  email: string;
}

async function getUser(id: number): Promise<User> {
  // axios.get<T> の T がレスポンスの data プロパティの型になる
  const response = await axios.get<User>(`https://api.example.com/users/${id}`);
  return response.data; // User 型として推論される
}

axios.get<User>()の戻り値はPromise<AxiosResponse<User>>です。response.dataにアクセスするとUser型として扱えます。

POSTリクエスト

TypeScript
interface CreateUserRequest {
  name: string;
  email: string;
}

interface CreateUserResponse {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

async function createUser(data: CreateUserRequest): Promise<CreateUserResponse> {
  // 第2引数にリクエストボディを渡す
  const response = await axios.post<CreateUserResponse>(
    "https://api.example.com/users",
    data
  );
  return response.data;
}

PUT・PATCH・DELETEリクエスト

TypeScript
interface UpdateUserRequest {
  name?: string;
  email?: string;
}

// PUT: リソース全体を更新
async function updateUser(id: number, data: UpdateUserRequest): Promise<User> {
  const response = await axios.put<User>(
    `https://api.example.com/users/${id}`,
    data
  );
  return response.data;
}

// PATCH: リソースを部分的に更新
async function patchUser(id: number, data: UpdateUserRequest): Promise<User> {
  const response = await axios.patch<User>(
    `https://api.example.com/users/${id}`,
    data
  );
  return response.data;
}

// DELETE: リソースを削除(レスポンスが空の場合は void を指定)
async function deleteUser(id: number): Promise<void> {
  await axios.delete(`https://api.example.com/users/${id}`);
}

AxiosResponse型を理解する

AxiosResponse<T>はAxiosが返すレスポンスオブジェクトの型です。data以外にも有用なプロパティがあります。

TypeScript
import axios, { AxiosResponse } from "axios";

async function fetchWithMeta(id: number): Promise<void> {
  const response: AxiosResponse<User> = await axios.get<User>(
    `https://api.example.com/users/${id}`
  );

  console.log(response.data);    // User 型: レスポンスボディ
  console.log(response.status);  // number: HTTPステータスコード (例: 200)
  console.log(response.headers); // AxiosResponseHeaders: レスポンスヘッダー
  console.log(response.config);  // InternalAxiosRequestConfig: リクエスト設定
}
プロパティ 内容
data T レスポンスボディ(ジェネリクスで指定した型)
status number HTTPステータスコード(200, 404 など)
statusText string ステータステキスト(”OK”, “Not Found” など)
headers AxiosResponseHeaders レスポンスヘッダー
config InternalAxiosRequestConfig このリクエストに使われた設定

エラーハンドリング:AxiosError型

Axiosのエラーハンドリングで多くの人が詰まるのが「エラーオブジェクトの型がunknownになる」問題です。AxiosError型とaxios.isAxiosError()を使うことで解決できます。

TypeScript
import axios, { AxiosError } from "axios";

interface ApiErrorResponse {
  message: string;
  code: string;
}

async function fetchUser(id: number): Promise<User | null> {
  try {
    const response = await axios.get<User>(
      `https://api.example.com/users/${id}`
    );
    return response.data;
  } catch (error) {
    // axios.isAxiosError() で型を絞り込む
    if (axios.isAxiosError<ApiErrorResponse>(error)) {
      // AxiosError<ApiErrorResponse> に型が絞り込まれる
      if (error.response) {
        // サーバーがエラーレスポンスを返した(4xx, 5xx)
        console.error("ステータス:", error.response.status);
        console.error("エラー内容:", error.response.data?.message);
      } else if (error.request) {
        // リクエストは送信されたがレスポンスが返ってこない(ネットワーク障害等)
        console.error("ネットワークエラー:", error.message);
      } else {
        // リクエストの設定段階でエラーが発生
        console.error("設定エラー:", error.message);
      }
    } else {
      // Axios以外のエラー(予期しないエラー)
      throw error;
    }
    return null;
  }
}

AxiosError<T>Tにはサーバーが返すエラーレスポンスの型を指定します。これによりerror.response.dataが型安全にアクセスできます。

3種類のAxiosエラー

  • error.responseがある: サーバーが4xx・5xxのレスポンスを返した(例: 404 Not Found、500 Internal Server Error)
  • error.requestがある: リクエストは送信されたが、サーバーからの応答がない(ネットワーク障害、タイムアウト)
  • どちらもない: URLが不正など、リクエストのセットアップ自体が失敗した

ステータスコード別のエラーハンドリング

TypeScript
import axios, { AxiosError } from "axios";

async function fetchUserWithStatusCheck(id: number): Promise<User> {
  try {
    const response = await axios.get<User>(
      `https://api.example.com/users/${id}`
    );
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error) && error.response) {
      switch (error.response.status) {
        case 400:
          throw new Error("リクエストが不正です");
        case 401:
          throw new Error("認証が必要です。ログインしてください");
        case 403:
          throw new Error("このリソースへのアクセス権限がありません");
        case 404:
          throw new Error(`ユーザー(ID: ${id})が見つかりません`);
        case 429:
          throw new Error("リクエスト回数が上限に達しました。しばらく待ってから再試行してください");
        case 500:
          throw new Error("サーバーエラーが発生しました");
        default:
          throw new Error(`HTTPエラー: ${error.response.status}`);
      }
    }
    throw error;
  }
}

エラーハンドリング全般についてはTypeScript エラーハンドリング完全ガイドも参照してください。

リクエスト設定の型定義

Axiosの各メソッドには設定オブジェクト(AxiosRequestConfig)を渡せます。ヘッダー・タイムアウト・クエリパラメータなどを型安全に指定できます。

TypeScript
import axios, { AxiosRequestConfig } from "axios";

// クエリパラメータの型を定義
interface UserListParams {
  page?: number;
  limit?: number;
  search?: string;
  sort?: "asc" | "desc";
}

interface UserListResponse {
  users: User[];
  total: number;
  page: number;
}

async function getUsers(params?: UserListParams): Promise<UserListResponse> {
  const config: AxiosRequestConfig = {
    params,                           // クエリパラメータ (?page=1&limit=20)
    timeout: 10000,                   // タイムアウト(ミリ秒)
    headers: {
      "Accept-Language": "ja",
      "X-Request-ID": crypto.randomUUID(),
    },
  };

  const response = await axios.get<UserListResponse>(
    "https://api.example.com/users",
    config
  );
  return response.data;
}

ファイルアップロード

TypeScript
async function uploadAvatar(userId: number, file: File): Promise<{ url: string }> {
  const formData = new FormData();
  formData.append("avatar", file);

  const response = await axios.post<{ url: string }>(
    `https://api.example.com/users/${userId}/avatar`,
    formData,
    {
      headers: { "Content-Type": "multipart/form-data" },
      onUploadProgress: (progressEvent) => {
        // AxiosProgressEvent 型
        if (progressEvent.total) {
          const percent = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          console.log(`アップロード進捗: ${percent}%`);
        }
      },
    }
  );
  return response.data;
}

インターセプター

インターセプターを使うと、すべてのリクエスト・レスポンスの前後に共通処理を挿入できます。認証トークンの付与やエラーのログ記録などに使います。

リクエストインターセプター

TypeScript
import axios, { InternalAxiosRequestConfig } from "axios";

// すべてのリクエストに認証ヘッダーを付与する
axios.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const token = localStorage.getItem("authToken");
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

レスポンスインターセプター

TypeScript
import axios, { AxiosResponse, AxiosError } from "axios";

// レスポンスを統一して処理する
axios.interceptors.response.use(
  (response: AxiosResponse) => {
    // 成功レスポンスをそのまま返す
    return response;
  },
  async (error: AxiosError) => {
    // 401 エラー時にトークンをリフレッシュしてリトライ
    if (error.response?.status === 401) {
      try {
        const newToken = await refreshAccessToken();
        localStorage.setItem("authToken", newToken);

        // 元のリクエストをリトライ
        if (error.config) {
          error.config.headers.Authorization = `Bearer ${newToken}`;
          return axios.request(error.config);
        }
      } catch {
        // リフレッシュ失敗 → ログイン画面へ
        window.location.href = "/login";
      }
    }
    return Promise.reject(error);
  }
);

async function refreshAccessToken(): Promise<string> {
  const response = await axios.post<{ accessToken: string }>(
    "/api/auth/refresh"
  );
  return response.data.accessToken;
}
インターセプターの削除
コンポーネントのアンマウント時などにインターセプターを削除しないとメモリリークの原因になります。axios.interceptors.request.use()の戻り値(ID)を保存しておき、axios.interceptors.request.eject(id)で削除してください。

カスタムAxiosインスタンス

グローバルなaxiosをそのまま使うと、ベースURLや認証設定が混在して管理しにくくなります。axios.create()でAPIごとにインスタンスを分けるのがベストプラクティスです。

src/lib/apiClient.ts
import axios from "axios";

const apiClient = axios.create({
  baseURL: "https://api.example.com",
  timeout: 10000,
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
});

// リクエストインターセプター
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem("authToken");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// レスポンスインターセプター
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (axios.isAxiosError(error) && error.response?.status === 401) {
      localStorage.removeItem("authToken");
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

export default apiClient;
src/api/users.ts
import apiClient from "../lib/apiClient";

export async function getUser(id: number): Promise<User> {
  const response = await apiClient.get<User>(`/users/${id}`);
  return response.data;
}

export async function createUser(data: CreateUserRequest): Promise<User> {
  const response = await apiClient.post<User>("/users", data);
  return response.data;
}

型安全なAPIクライアントクラスの設計

規模の大きなプロジェクトでは、APIのエンドポイントをクラスとして整理すると見通しが良くなります。

src/api/UserApi.ts
import axios, { AxiosInstance, AxiosError } from "axios";

interface User {
  id: number;
  name: string;
  email: string;
}

interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
}

interface UserListParams {
  page?: number;
  limit?: number;
  search?: string;
}

interface UserListResponse {
  users: User[];
  total: number;
  page: number;
  totalPages: number;
}

// API エラーを統一して扱うカスタムエラークラス
class ApiError extends Error {
  constructor(
    public readonly statusCode: number,
    message: string,
    public readonly originalError?: AxiosError
  ) {
    super(message);
    this.name = "ApiError";
  }
}

class UserApi {
  constructor(private readonly client: AxiosInstance) {}

  async list(params?: UserListParams): Promise<UserListResponse> {
    const response = await this.client.get<UserListResponse>("/users", { params });
    return response.data;
  }

  async get(id: number): Promise<User> {
    const response = await this.client.get<User>(`/users/${id}`);
    return response.data;
  }

  async create(data: CreateUserRequest): Promise<User> {
    const response = await this.client.post<User>("/users", data);
    return response.data;
  }

  async update(id: number, data: Partial<CreateUserRequest>): Promise<User> {
    const response = await this.client.patch<User>(`/users/${id}`, data);
    return response.data;
  }

  async delete(id: number): Promise<void> {
    await this.client.delete(`/users/${id}`);
  }
}

// インスタンスをエクスポート
const axiosInstance = axios.create({
  baseURL: "https://api.example.com",
  timeout: 10000,
});

export const userApi = new UserApi(axiosInstance);
export { ApiError };
使用例
import { userApi, ApiError } from "./api/UserApi";

async function loadUsers() {
  try {
    const result = await userApi.list({ page: 1, limit: 20 });
    console.log(`${result.total}件中 ${result.users.length}件を取得`);
    return result.users;
  } catch (error) {
    if (error instanceof ApiError) {
      console.error(`APIエラー (${error.statusCode}): ${error.message}`);
    }
    throw error;
  }
}

Reactとの組み合わせパターン

ReactでAxiosを使う場合、カスタムフックに切り出すと再利用しやすくなります。

src/hooks/useUser.ts
import { useState, useEffect } from "react";
import axios, { AxiosError } from "axios";
import apiClient from "../lib/apiClient";

interface UseUserResult {
  user: User | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}

export function useUser(id: number): UseUserResult {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [count, setCount] = useState(0);

  useEffect(() => {
    // AbortController でクリーンアップ
    const controller = new AbortController();

    const fetchUser = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await apiClient.get<User>(`/users/${id}`, {
          signal: controller.signal,
        });
        setUser(response.data);
      } catch (err) {
        if (axios.isCancel(err)) return; // アンマウント時のキャンセルは無視
        if (axios.isAxiosError(err)) {
          setError(err.response?.data?.message ?? "データの取得に失敗しました");
        } else {
          setError("予期しないエラーが発生しました");
        }
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
    return () => controller.abort();
  }, [id, count]);

  return { user, loading, error, refetch: () => setCount((c) => c + 1) };
}

ReactのHooks型定義についてはTypeScript × React Hooks の型定義完全ガイドも参照してください。

よくある型エラーと対処法

型エラー①:response.dataがanyになる

NG: 型の指定なし
// NG: response.data が any 型になる
const response = await axios.get("/users/1");
response.data.nonExistentProperty; // 型エラーにならない(any のため)
OK: ジェネリクスで型を指定
// OK: ジェネリクスで型を明示する
const response = await axios.get<User>("/users/1");
response.data.nonExistentProperty; // コンパイルエラー(User にそのプロパティがない)

型エラー②:AxiosErrorのresponse.dataにアクセスできない

NG: 型の絞り込みなし
catch (error) {
  // error は unknown 型 → プロパティに直接アクセスできない
  console.log(error.response.data); // NG: Object is of type unknown
}
OK: isAxiosError()で絞り込む
catch (error) {
  if (axios.isAxiosError<ApiErrorResponse>(error)) {
    // AxiosError<ApiErrorResponse> に絞り込まれる
    console.log(error.response?.data?.message); // OK
  }
}

型エラー③:インターセプターで型エラーが出る

NG: 古い型定義を使っている
// Axios v1.x では AxiosRequestConfig → InternalAxiosRequestConfig に変更
axios.interceptors.request.use((config: AxiosRequestConfig) => { // 型エラー
  config.headers.Authorization = "Bearer token";
  return config;
});
OK: InternalAxiosRequestConfigを使う
import { InternalAxiosRequestConfig } from "axios";

axios.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  config.headers.Authorization = "Bearer token";
  return config;
});
Axios v0.x → v1.x の破壊的変更
Axios v1.0でリクエスト設定の型がAxiosRequestConfigからInternalAxiosRequestConfigに変更されました。v0.x系からアップグレードした際にインターセプター周りで型エラーが出る場合はこの変更が原因のことがあります。

型エラー④:カスタムリクエスト設定フィールドを追加したい

TypeScript(型の拡張)
// axios の型を拡張してカスタムフィールドを追加
declare module "axios" {
  interface InternalAxiosRequestConfig {
    retryCount?: number;
    skipAuth?: boolean;
  }
}

// 使用例
apiClient.interceptors.request.use((config) => {
  if (config.skipAuth) {
    // 認証ヘッダーを付与しない
    return config;
  }
  config.headers.Authorization = `Bearer ${getToken()}`;
  return config;
});

// リクエスト時にカスタムフィールドを渡せる
apiClient.get("/public/data", { skipAuth: true });

モジュール拡張の詳細はTypeScript 型定義ファイル(.d.ts)完全ガイドを参照してください。

テストでAxiosをモック化する

テストコードでは実際のHTTPリクエストを発行せず、axios-mock-adapterjest.mock()でモック化するのが一般的です。

ターミナル
npm install --save-dev axios-mock-adapter
TypeScript(テストファイル)
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { getUser } from "./api/users";

const mock = new MockAdapter(axios);

describe("getUser", () => {
  afterEach(() => {
    mock.reset(); // 各テスト後にモックをリセット
  });

  it("ユーザーを正常に取得できる", async () => {
    const mockUser: User = { id: 1, name: "田中太郎", email: "tanaka@example.com" };
    mock.onGet("/users/1").reply(200, mockUser);

    const user = await getUser(1);

    expect(user.id).toBe(1);
    expect(user.name).toBe("田中太郎");
  });

  it("404エラー時にnullを返す", async () => {
    mock.onGet("/users/999").reply(404, { message: "Not Found" });

    const user = await getUser(999);

    expect(user).toBeNull();
  });
});

TypeScriptでのテスト全般についてはTypeScript Jest・Vitestテスト完全ガイドを参照してください。

まとめ

AxiosをTypeScriptで使う際のポイントをまとめます。

項目 ポイント
レスポンス型 axios.get<T>()のジェネリクスでデータ型を指定する
エラー処理 axios.isAxiosError()で型を絞り込み、error.responseerror.requestで原因を分類する
インターセプター 認証・ログ・エラー共通処理を一箇所にまとめる。Axios v1.xはInternalAxiosRequestConfigを使う
カスタムインスタンス axios.create()でAPIごとのインスタンスを作り、設定を分離する
APIクライアント クラスにまとめてエンドポイントを整理するとコードの見通しが良くなる
React連携 カスタムフックに切り出し、AbortControllerでクリーンアップを忘れずに

AxiosはTypeScriptの型システムと非常に相性が良く、ジェネリクスとAxiosErrorを使いこなすだけで大幅に型安全性が向上します。まずはカスタムインスタンスの作成とエラーハンドリングのパターンを取り入れ、プロジェクトのニーズに合わせてAPIクライアントを育てていくのがおすすめです。

AxiosでAPIを叩く際の入力バリデーションにはTypeScript × Zod 完全ガイドを、Node.js環境での設定についてはTypeScript × Node.js 完全ガイドもあわせてご覧ください。