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も型定義はありますが、エラー処理・インターセプター・タイムアウトを自前実装する必要があります。Axiosはこれらをすぐに使える形で提供しており、実務規模のアプリケーションでは開発コストを大幅に削減できます。インストールと初期設定
Axiosはv1.0以降、型定義が本体に同梱されています。@types/axiosは不要です。
# npm npm install axios # yarn yarn add axios # pnpm pnpm add axios
インストール後、tsconfig.jsonに以下の設定が入っているか確認してください。
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node"
}
}
esModuleInterop: trueが必須esModuleInteropがfalseの場合、import axios from "axios"がコンパイルエラーになります。import * as axios from "axios"と書く方法もありますが、esModuleInterop: trueにしておくほうがコードがシンプルになります。tsconfig.jsonの設定方法も参考にしてください。基本的なリクエストと型定義
Axiosの各メソッドはジェネリクスを受け取り、AxiosResponse<T>を返します。Tにレスポンスデータの型を渡すことで型安全なアクセスができます。
GETリクエスト
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リクエスト
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リクエスト
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以外にも有用なプロパティがあります。
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()を使うことで解決できます。
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が型安全にアクセスできます。
error.responseがある: サーバーが4xx・5xxのレスポンスを返した(例: 404 Not Found、500 Internal Server Error)error.requestがある: リクエストは送信されたが、サーバーからの応答がない(ネットワーク障害、タイムアウト)- どちらもない: URLが不正など、リクエストのセットアップ自体が失敗した
ステータスコード別のエラーハンドリング
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)を渡せます。ヘッダー・タイムアウト・クエリパラメータなどを型安全に指定できます。
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;
}
ファイルアップロード
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;
}
インターセプター
インターセプターを使うと、すべてのリクエスト・レスポンスの前後に共通処理を挿入できます。認証トークンの付与やエラーのログ記録などに使います。
リクエストインターセプター
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);
}
);
レスポンスインターセプター
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ごとにインスタンスを分けるのがベストプラクティスです。
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;
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のエンドポイントをクラスとして整理すると見通しが良くなります。
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を使う場合、カスタムフックに切り出すと再利用しやすくなります。
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: response.data が any 型になる
const response = await axios.get("/users/1");
response.data.nonExistentProperty; // 型エラーにならない(any のため)
// OK: ジェネリクスで型を明示する
const response = await axios.get<User>("/users/1");
response.data.nonExistentProperty; // コンパイルエラー(User にそのプロパティがない)
型エラー②:AxiosErrorのresponse.dataにアクセスできない
catch (error) {
// error は unknown 型 → プロパティに直接アクセスできない
console.log(error.response.data); // NG: Object is of type unknown
}
catch (error) {
if (axios.isAxiosError<ApiErrorResponse>(error)) {
// AxiosError<ApiErrorResponse> に絞り込まれる
console.log(error.response?.data?.message); // OK
}
}
型エラー③:インターセプターで型エラーが出る
// Axios v1.x では AxiosRequestConfig → InternalAxiosRequestConfig に変更
axios.interceptors.request.use((config: AxiosRequestConfig) => { // 型エラー
config.headers.Authorization = "Bearer token";
return config;
});
import { InternalAxiosRequestConfig } from "axios";
axios.interceptors.request.use((config: InternalAxiosRequestConfig) => {
config.headers.Authorization = "Bearer token";
return config;
});
Axios v1.0でリクエスト設定の型が
AxiosRequestConfigからInternalAxiosRequestConfigに変更されました。v0.x系からアップグレードした際にインターセプター周りで型エラーが出る場合はこの変更が原因のことがあります。型エラー④:カスタムリクエスト設定フィールドを追加したい
// 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-adapterやjest.mock()でモック化するのが一般的です。
npm install --save-dev axios-mock-adapter
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.response・error.requestで原因を分類する |
| インターセプター | 認証・ログ・エラー共通処理を一箇所にまとめる。Axios v1.xはInternalAxiosRequestConfigを使う |
| カスタムインスタンス | axios.create()でAPIごとのインスタンスを作り、設定を分離する |
| APIクライアント | クラスにまとめてエンドポイントを整理するとコードの見通しが良くなる |
| React連携 | カスタムフックに切り出し、AbortControllerでクリーンアップを忘れずに |
AxiosはTypeScriptの型システムと非常に相性が良く、ジェネリクスとAxiosErrorを使いこなすだけで大幅に型安全性が向上します。まずはカスタムインスタンスの作成とエラーハンドリングのパターンを取り入れ、プロジェクトのニーズに合わせてAPIクライアントを育てていくのがおすすめです。
AxiosでAPIを叩く際の入力バリデーションにはTypeScript × Zod 完全ガイドを、Node.js環境での設定についてはTypeScript × Node.js 完全ガイドもあわせてご覧ください。
