zukucode
主にWEB関連の情報を技術メモとして発信しています。

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形式でリクエストをする場合の処理も作成しています。

putdeleteなども必要な場合は同じ要領で作成します。

// 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); // エラーメッセージを表示
}

関連記事