TypeScript fetchの共通処理を実装する
TypeScript
のプロジェクトでAPI
をコールする際に使用するfetch
の処理をエラーハンドリングなども含めて共通処理として実装します。
型の定義
まずはAPI
からのレスポンスの共通の型を定義します。
「正常時または異常時」の型を定義します。
正常時はAPI
によって型は変わるため、ジェネリックで指定できるようにして、異常時はシステム共通の型でレスポンスされることを想定しています。
正常時または異常時にそれぞれの型でdata
にアクセスするためにok
フィールドをboolean
ではなくtrue
またはfalse
固定で定義します。
// APIのレスポンスの型
export type ApiResult<T> = ApiSuccessResult<T> | ApiErrorResult;
// 正常時の型
export type ApiSuccessResult<T> = {
data: T;
ok: true;
};
// 異常時の型(エラーコードとエラーメッセージがレスポンスされることを想定)
export type ApiErrorResult = {
data: { code?: string; message: string; };
ok: false;
};
レスポンスの読み込み
fetch
の戻り値はResponse
という型で、レスポンスデータだけでなく、ヘッダ情報やステータスコードなどの情報も持っています。
API
からのデータはJSON形式でレスポンスされることを想定して、以下のような処理を実装します。
この時点では正常終了時の型はわからないのでunknown
としています。
const handleResponse = async (response: Response): Promise<ApiResult<unknown>> => {
const isJson = response.headers.get('content-type')?.includes('application/json');
// JSON形式でデータを取得する
const data = isJson ? await response.json() : null;
if (!response.ok) {
// 異常時(ステータスコードが400~,500~)
// レスポンスデータのmessageが存在しない場合はステータスのテキストを設定する
const error = (data && data.message) || response.statusText;
return {
ok: false,
data: {
message: error,
code: data?.code,
},
};
}
// 正常終了
return { ok: true, data };
};
リクエスト処理の実装
fetch
はそのまま使用するのではなく、以下のようにラッパー処理として実装しておきます。
fetch
ではなくaxios
などを使用する場合はなるべくこの処理だけ修正すればいいようにします。
const http = async (url: string, init?: RequestInit): Promise<Response> => {
const response = await fetch(url, init);
return response;
};
以下のように、リクエストメソッドごとに共通処理を作成します。
ファイルのアップロードなど、multipart
形式でリクエストをする場合の処理も作成しています。
put
やdelete
なども必要な場合は同じ要領で作成します。
// getリクエスト
export const get = async (url: string): Promise<ApiResult<unknown>> => {
const data = await http(url, {
method: 'GET',
});
return await handleResponse(data);
};
// postリクエスト
export const post = async (
url: string,
body: Record<string, unknown> | Record<string, unknown>[],
): Promise<ApiResult<unknown>> => {
const data = await http(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return await handleResponse(data);
};
// multipart形式のリクエスト
// JSON形式のパラメータをFormDataに変換してリクエストする
export const postMultiPart = async (
url: string,
body: Record<string, string | Blob | string[] | Blob[]>,
): Promise<ApiResult<unknown>> => {
const data = await http(url, {
method: 'POST',
body: Object.entries(body).reduce((formData, [key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => formData.append(key, v));
} else {
formData.append(key, value);
}
return formData;
}, new FormData()),
});
return await handleResponse(data);
};
上記の共通処理を使用して、API
ごとに固有の引数と戻り値を持つファンクションを作成します。
以下の例は、idで本を検索するAPI
をコールしています。
正常時のレスポンスデータがBook
型であることを前提としているため、as
を使用して型変換をしています。
レスポンスデータが本当にBook
型であるかどうかチェックを行いたい場合は、このタイミングでチェック処理を実装します。
レスポンスデータの型チェックについてはAjvでAPIのレスポンスの型チェックを行うで紹介しています。
type Book = {
id: number;
title: string;
};
export const getBookById = async (
param: { id: number },
): Promise<ApiResult<Book>> => {
const response = await post('/api/books/findById', param);
return response.ok ? { ...response, data: response.data as Book } : response;
};
以上で、共通処理の実装は完了です。
使い方
以下のように使用します。
if
文で正常時と異常時を分岐させれば、それぞれの処理で正常時の型、異常時の型でアクセスが可能です。
// APIで本を取得
const result = await getBookById({ id: 123 });
if (result.ok) {
console.log(result.data.title); // 本のタイトルを表示
} else {
console.log(result.data.message); // エラーメッセージを表示
}