TypeScriptで実現するAPIエラーコードの型安全な標準化
Photo by Iñaki del Olmo on Unsplash
はじめに
APIエラーを扱う際、エラーコードやメッセージの命名規則がプロジェクト内でバラバラになりがちです。特にチーム開発では、以下のような問題が起こりがちです。
- エラーコードの重複・不統一
似たようなエラーなのに、サービスごとに「USER_NOT_FOUND」だったり「userMissing」だったり。 - 手動で文字列を入力するためのミス
直接"auth:user-not-found"のような文字列を打ち込むと、typo(タイプミス)を起こしやすい。 - エラー一覧のメンテナンスが手間
どの機能でどのようなエラーが出るのかを読んで理解するのが面倒。
これらを解決しつつ、TypeScriptの型チェックとエディタのオートコンプリートを活かして「統一されたエラーコードと人間向けメッセージ」を定義する方法を紹介します。
コード例:テンプレートリテラル型でエラーコードを定義する
// 1. 機能(Feature)の一覧
export type Feature = "auth" | "campaign" | "payments" | "user" | "validation" | "network";
// 2. エラータイプの一覧
export type ErrorType =
| "user-not-found"
| "campaign-creation-failed"
| "too-many-requests"
| "permission-denied"
| "invalid-input"
| "timeout"
| "network-error"
| "unknown";
// 3. エラーコードは `${Feature}:${ErrorType}` の形とする
export type ErrorCode = `${Feature}:${ErrorType}`;
// 4. エラーコード → 人間向けメッセージ を返すユーティリティ関数
export const toHumanReadable = (errorCode: ErrorCode): string => {
switch (errorCode) {
// auth 関連のエラー
case "auth:user-not-found":
return "お使いのメールアドレスは登録されていません。アカウントを作成してください。";
case "auth:too-many-requests":
return "ログイン試行が多すぎます。5分後に再度お試しください。";
// campaign 関連のエラー
case "campaign:campaign-creation-failed":
return "キャンペーンの作成に失敗しました。入力内容を確認し、再度お試しください。";
// payments 関連のエラー
case "payments:permission-denied":
return "お支払いに必要な権限がありません。管理者にお問い合わせください。";
// validation 関連のエラー
case "validation:invalid-input":
return "入力値に誤りがあります。すべての必須項目を正しく入力してください。";
// network 関連のエラー
case "network:network-error":
return "ネットワークエラーが発生しました。通信環境を確認して、再度お試しください。";
case "network:timeout":
return "サーバーへのリクエストがタイムアウトしました。しばらく待ってから再度お試しください。";
// デフォルト(unknown)
case "auth:unknown":
case "campaign:unknown":
case "payments:unknown":
case "user:unknown":
case "validation:unknown":
case "network:unknown":
return "不明なエラーが発生しました。時間をおいて再度お試しください。";
default:
// 型上ありえないコードが来た場合も、フォールバックメッセージを返す
return "エラーが発生しました。";
}
};
ポイント解説
FeatureとErrorTypeを分けて定義
まず、サービスやドメインごとに扱いたい機能(Feature)を列挙します。
次に、どのようなエラータイプがあるのかをexport type Feature = "auth" | "campaign" | "payments" | …;ErrorTypeとして列挙します。export type ErrorType = "user-not-found" | "too-many-requests" | "invalid-input" | …;- テンプレートリテラル型で
ErrorCodeを合成
TypeScript のテンプレートリテラル型を使って、"${Feature}:${ErrorType}"という文字列型を自動生成します。
こうすると、例えばexport type ErrorCode = `${Feature}:${ErrorType}`;"auth:user-not-found"や"campaign:invalid-input"といった形のエラーコードだけが型として通ります。 - 誤った組み合わせ(例:
"auth:campaign-creation-failed")はコンパイルエラーになります。 - エディタのオートコンプリートで、
FeatureとErrorTypeの候補がサジェストされるため、typoをかなり減らせます。
- 誤った組み合わせ(例:
toHumanReadableで紐づけるswitch文でErrorCodeを受け取り、適宜人間に伝わりやすいメッセージを返します。
ここで「export const toHumanReadable = (errorCode: ErrorCode): string => {
switch (errorCode) {
case "auth:too-many-requests":
return "ログイン試行が多すぎます…";
// 他のケース…
default:
return "エラーが発生しました。";
}
};case "auth:unknown":のように、Featureごとのunknownを用意しておくことで、フォールバックの統一感を出すこともできます。
さらなる汎用性を持たせるアイデア
単純に switch 文で書くだけでも十分ですが、大規模プロジェクトになると以下のような拡張を検討すると良いでしょう。
1. オブジェクトマップ形式に切り替え
// 1. ErrorCode をキーにしてメッセージをまとめたマップ
const errorMessages: Record<ErrorCode, string> = {
"auth:user-not-found": "お使いのメールアドレスは登録されていません。",
"auth:too-many-requests": "ログイン試行が多すぎます…",
"campaign:campaign-creation-failed": "キャンペーンの作成に失敗しました。",
"payments:permission-denied": "お支払いに必要な権限がありません。",
"validation:invalid-input": "入力値に誤りがあります。",
"network:network-error": "ネットワークエラーが発生しました。",
"network:timeout": "リクエストがタイムアウトしました。",
// unknown系
"auth:unknown": "不明なエラーが発生しました。",
"campaign:unknown": "不明なエラーが発生しました。",
"payments:unknown": "不明なエラーが発生しました。",
"validation:unknown": "不明なエラーが発生しました。",
"network:unknown": "不明なエラーが発生しました。",
// 必要に応じて "user:unknown" なども追加
};
// 2. マップからメッセージを返す関数
export const toHumanReadable = (code: ErrorCode): string => {
return errorMessages[code] ?? "エラーが発生しました。";
};- 単一のオブジェクト にすべてのエラーコードとメッセージを集約することで、保守性が上がります。
- 新しいエラーを追加したいときは
errorMessagesにキーとメッセージを足すだけです。
2. 共通のエラータイプをさらに抽象化して使い回す
たとえば「入力値のバリデーションに失敗した」「リクエスト先のサービスが落ちている」など、どの機能でも発生しやすいエラーを generic カテゴリとしてまとめてもOKです。
export type Feature = "auth" | "campaign" | "payments" | "user" | "generic";
export type ErrorType =
| "user-not-found"
| "campaign-creation-failed"
| "too-many-requests"
| "permission-denied"
| "invalid-input"
| "timeout"
| "network-error"
| "service-unavailable"
| "unknown";
// "generic:service-unavailable" のように使えるgeneric:service-unavailable… バックエンド全体が一時的に使えないときgeneric:invalid-input… フロントエンドでの入力チェックに失敗したときgeneric:timeout… どの機能であっても「タイムアウト」をまとめて扱いたいとき
これにより「特定の機能で予測されるエラー」と「どの機能でも発生し得る汎用エラー」を分けて管理できます。
利用例:エラーを投げる・受け取るときのコード
API レスポンス側(バックエンド・フロントエンド共通)
// 例: API サーバー側でエラーを返す型
interface ApiErrorResponse {
code: ErrorCode;
message: string; // toHumanReadable(code) をセットしておく
details?: any; // バリデーションエラーの詳細など
}
// 例: バリデーションエラーを返す処理
function validateUserInput(data: any): ApiErrorResponse | null {
if (!data.email) {
return {
code: "generic:invalid-input",
message: toHumanReadable("generic:invalid-input"),
details: { field: "email", reason: "required" },
};
}
return null;
}クライアント側(例:Fetch した結果を処理)
async function fetchCampaign(campaignId: string) {
const res = await fetch(`/api/campaign/${campaignId}`);
const json = await res.json();
if (!res.ok) {
// サーバーから返ってきたエラーコードを使ってメッセージを取得
const apiErr = json as ApiErrorResponse;
alert(toHumanReadable(apiErr.code));
return;
}
// 正常処理
return json;
}- バックエンドで
ApiErrorResponse.codeにErrorCodeを付与し、フロントエンドでは受け取った文字列を再びtoHumanReadableに投げるだけで、一貫したメッセージ表示が可能です。 - 型安全が保たれるため、万が一バックエンド側で「
"auth:unknown-error"」のように定義していないエラーコードを送ってきても、コンパイル時に検出できます。
まとめ
- テンプレートリテラル型 (
${Feature}:${ErrorType}) を使うことで、エラーコードの候補を絞り込み、オートコンプリートを実現できる。 - エラーコードとメッセージを一元管理することで、今後の機能追加・保守が楽になる。
- 共通エラー (
generic:xxx)、機能別エラー(auth:xxx、campaign:xxx)を分けることで、整理しやすいディレクトリ構成になる。 - バックエンド/フロントエンドで同じ型を共有すれば、実行前にバグを防げる。
このように TypeScript の型システムを活用してエラーコードを定義し、オートコンプリート・型チェックをフル活用 することで、エラー処理の統一感と開発効率が大幅に向上します。ぜひプロジェクトに取り入れてみてください。