AxiosをTypeScriptで型安全に利用する【React + TypeScript】

AxiosはPromiseベースでHTTPリクエストを扱うことのできるHTTPクライアントです。

TypeScriptで型安全にAxiosを使用するためのポイントをまとめました。

動作確認のために、React+TypeScriptのフロントから実際にHTTPリクエストを送り、取得したデータをカード型コンポーネントに表示しています。記事と合わせてご覧ください。

動作例 GitHub

wikipediaの記事情報をAPIから取得しカードコンポーネントに表示

完成版のコードを早く見たい人は上記のGithubか、5章から読んでください。

動作環境、ファイル構成など

Axiosはnode.js環境、ブラウザー環境ともに同じコードで動作するHTTPクライアントです。インストールがお済みで無い方は、

npm install axiosまたは yarn add axiosでインストールしてください。

本記事のコードは以下の環境で動作を確認しています。

バージョン情報
  • typescript 4.9.3
  • axios 1.7.2
  • react 18.2.0

本記事の内容は以下のディレクトリ、ファイル構成を前提としています(GitHub)。フロントエンドにReactを利用しましたが、axiosをtypescriptで利用する方法は、reactコンポーネントに依存しませんので応用が可能です。

.
└── src/
├── components/
│ └── WikiInfo.tsx // 取得データ表示用コンポーネント
└── utils/wikipedia/
└── getWikiSummary.ts // データフェッチを行う関数本体

REST APIの例として、wikipediaの記事情報を取得できるMediaWiki APIWikiMedia REST APIを利用しています。エンドポイントはhttps://en.wikipedia.org/api/rest_v1/です。

利用は無料ですが、ご自身でのご利用の際にはエチケットにご注意ください。

レスポンスにはAxiosResponse型を使用する

TypeScriptでHTTPリクエストを扱うとき、まず最初にレスポンスを受け取る型を決めなければなりません。レスポンスの型には、Axiosに用意されているAxiosResponse型を利用します。

今回例に挙げているMediaWikiAPIを使用して、記事概要の情報を取得するには、以下のURLにリクエストを送ります。

  • Wikipedia英語版:https://en.wikipedia.org/api/rest_v1/page/summary/記事タイトル
  • Wikipedia日本語版:https://ja.wikipedia.org/api/rest_v1/page/summary/記事タイトル

AxiosResponse型でレスポンスを受け取りましょう。

{"code":"import axios, { type AxiosResponse } from 'axios';\n\/\/ \u30bf\u30a4\u30c8\u30eb 'Earth'\u306e\u82f1\u8a9e\u7248Wikipedia\u306eURL\u3092\u6307\u5b9a\u3059\u308b\u4f8b\nconst url = 'https:\/\/en.wikipedia.org\/api\/rest_v1\/page\/summary\/Earth';\nconst res: AxiosResponse = await axios(url);\nconsole.log(res);\n\n\/\/ {data: {\u2026}, status: 200, statusText: '', headers: AxiosHeaders, config: {\u2026}, \u2026}","language":"typescript","id":2,"filename":""}

AxiosResponse型はGenericsであり、もう1つ型を指定して使用しますが、エンドポイントの指定が間違っていたり、ネットワーク障害があったりすると、正常なレスポンスで期待されるデータの型ではない型が返ってきます。

後ほどエラーハンドリングを行うので、ここではanyを指定しておきます。

オプションはAxiosRequestConfig型を使用する

本記事では簡単のためGETメソッドのみ紹介しますが、他のメソッドでHTTPリクエストを行う場合にも応用が効くように、リクエストの種類・送信する情報などをオブジェクトに分離しておく方が良いです。

リクエストの種類や送信する情報をまとめたオブジェクトは、AxiosRequestConfig型を使用して定義します。

{"code":"import axios, { type AxiosResponse, type AxiosRequestConfig } from 'axios';\nconst url = 'https:\/\/en.wikipedia.org\/api\/rest_v1\/page\/summary\/';\nconst title = 'Earth';\n\/\/ \u30ea\u30af\u30a8\u30b9\u30c8\u306e\u30b3\u30f3\u30d5\u30a3\u30b0\u3092\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306b\u5206\u96e2\nconst options: AxiosRequestConfig = {\n  url: `${url}\/${title}`,\n  method: 'GET',\n  \/\/ POST\u306a\u3069\u306e\u6642\u306f\u3053\u3053\u306b\u8272\u3005\u304b\u304f\n};\n\/\/ \u30ea\u30af\u30a8\u30b9\u30c8\u3092\u9001\u308b\nconst res: AxiosResponse = await axios(options);","language":"typescript","id":2,"filename":""}

TypeScriptで型安全なエラーハンドリングを行う

TypeScriptを使用する時に厄介なのがエラーハンドリングですね。私自身が調べた限りではtry {...} catch{...}構文のエラーハンドリングが最も簡単に実装できると感じましたのでそれを紹介します。

AxiosエラーかどうかをisAxiosErrorで判定する

HTTPリクエストを行ってエラーが発生した時、発生したエラーが、Axiosが認識できるエラー(Axiosエラー)なのかどうかを、isAxiosErrorメソッドで判別できます。

Axiosはネットワークエラーなども幅広く認識できますが、全てのエラーがAxiosエラーである前提でコーディングしてしまうと、異なる種類’のエラーが発生した場合にコードがクラッシュしてしまいます。

例えば、Axiosエラーと他のエラーを分類せずにエラーメッセージを取得しようとする以下のコードは、クラッシュすることがあります。

{"code":"try {\n  const res: AxiosResponse = await axios(url);\n}catch(error: any){\n  const { status, message } = error;\n  console.dir( `${status} : ${message}`);\n}","language":"typescript","id":2,"filename":""}

エラーレスポンスに含まれるステータスコード(status)やエラーメッセージ(message)は、Axiosエラーにしか存在しないデータです。

実際に、「unknown型のエラーに対してstatus, messageが取得できません」という旨のエラーが起きることが報告されています。

次節に記載するように、間違いなくAxiosErrorエラーであることが確定したブロック内でエラーを扱いましょう。

isAxiosErrorメソッドはaxios v0.21.0以降で利用可能です。axiosのバージョンが古い場合はバージョンアップをしてください。

isAxiosErrorとtry…catch構文で対応する

前置きが長くなりましたが、正しいエラーハンドリングを行うコードは以下のようになります。

{"code":"try{\n  const res: AxiosResponse = await axios(url);\n  \/\/ \u30ec\u30b9\u30dd\u30f3\u30b9\u3092\u6271\u3046\n}catch(error: any){\n  if(isAxiosError(error)){\n    \/\/ \u3053\u306e\u30d6\u30ed\u30c3\u30af\u5185\u306f\u3001\u767a\u751f\u3057\u305f\u30a8\u30e9\u30fc\u304cAxios\u30a8\u30e9\u30fc\u3060\u3068\u78ba\u5b9a\u3057\u3066\u3044\u308b\n    const { status, message } = error;\n    console.dir(`${status} : ${message}`);\n  }else{\n    console.dir('Some unknown Error Occurred.');\n  }\n}","language":"typescript","id":2,"filename":""}

上記のコードでは、Axiosエラーではないエラーが発生した場合にもクラッシュせず、その情報をユーザーに伝えることもできています。

ユーティリティ関数として実装しよう

以上のことを踏まえて、axiosを使用し型安全なデータフェッチを行う関数を実装しましょう。

今回は、wikipediaの記事情報を取得する関数として実装してみます。getWikiSummary関数は、wikipediaの(URLベースの)タイトルを与えると、以下のWikiInfo型で定義されたデータを返すユーティリティです。

フルバージョンの実装はGitHubを参照してください。

{"code":"export type WikiInfo = {\n  url: string;  \/\/ \u8a18\u4e8b\u306eURL\n  title: string; \/\/ \u8a18\u4e8b\u306e\u30bf\u30a4\u30c8\u30eb \n  summary: string;  \/\/ \u8a18\u4e8b\u306e\u6982\u8981\n  thumbnail: string; \/\/ \u8a18\u4e8b\u30b5\u30e0\u30cd\u30a4\u30eb\u753b\u50cf\u306eURL\n  error?: string; \/\/ \u30a8\u30e9\u30fc\u5185\u5bb9\u3092\u8a18\u9332\u3059\u308b\u305f\u3081\u306e\u30d7\u30ed\u30d1\u30c6\u30a3\n};","language":"typescript","id":2,"filename":""}

getWikiSummary関数の実装を記載します。axiosは非同期にリクエストを行うため、getWikiSummary自体もPromiseを返す非同期関数となることに注意してください。

{"filename":"getWikiSummary.ts","code":"\/\/ import\u306f\u7701\u7565\nexport const getWikiSummary = async (\n  lang: 'en' | 'ja', \/\/ \u8a00\u8a9e\u3092\u6307\u5b9a\n  title: string \/\/ wiki\u306e\u30bf\u30a4\u30c8\u30eb\uff08URL\u30d9\u30fc\u30b9\u306e\u30bf\u30a4\u30c8\u30eb\uff09\u3092\u6307\u5b9a\n): Promise => {\n  const wikiInfo: WikiInfo = {\n    url: '',\n    title: '',\n    summary: '',\n    thumbnail: '',\n  };\n  \/\/ \u82f1\u8a9e\u7248wiki\u3068\u65e5\u672c\u8a9e\u7248wiki\u4e21\u65b9\u306b\u5bfe\u5fdc\n  const url: string = \n    lang === 'en'\n      ? 'https:\/\/en.wikipedia.org\/api\/rest_v1\/page\/summary'\n      : 'https:\/\/ja.wikipedia.org\/api\/rest_v1\/page\/summary';\n  const oprions: AxiosRequestConfig = {\n    url: `${url}\/${title}`,\n    method: 'GET'\n  };\n  try{\n    const res: AxiosResponse = await axios(options);\n    const items = res.data; \/\/ \u53d6\u5f97\u3057\u305f\u30c7\u30fc\u30bf\u306f.data\u306b\u3042\u308b\n    \n    \/\/ API\u304c\u8fd4\u3059\u30c7\u30fc\u30bf\u69cb\u9020\u3092\u898b\u3066\u5b9f\u88c5\u3059\u308b\n    wikiInfo.url = items.content_urls.desktop.page;\n    wikiInfo.title = items.title;\n    wikiInfo.summary = items.extract_html;\n    wikiInfo.thumbnail = items.thumbnail.source;\n  } catch (error: any){\n    if (isAxiosError(error)){\n      const { status, message } = error;\n      \/\/ \u30a8\u30e9\u30fc\u30e1\u30c3\u30bb\u30fc\u30b8\u3092error\u30d7\u30ed\u30d1\u30c6\u30a3\u306b\u30bb\u30c3\u30c8\n      wikiInfo.error = `Error: status: ${status}, ${message}`;\n    } else {\n        wikiInfo.error = `Some unknown Error Occurred.`;\n    }\n  }\n  return wikiInfo;\n};","language":"typescript","id":2}

TypeScriptにおいて、Promiseベースの非同期関数が返す型はPromise<T>です。関数がどんな型のデータを返すかという情報より上位に、「非同期処理が完了しているかどうか」という状態があるわけですね。

Reactコンポーネントに表示してみる

ユーティリティ関数が実装できたので、動作確認としてReactのコンポーネントWikiInfoで使用してみます。

今回は(古典的ですが)useEffect()を使用して、コンポーネントマウント時 + 記事タイトルtitleが変化時にデータフェッチがトリガーされるようにしました。

取得されたデータはコンポーネントにローカルなstateとして保持しています。

{"filename":"WikiInfo.tsx","code":"type WikiInfoComponentProps = {\n  title: string;\n};\nexport function WikiInfo({ title }: WikiInfoComponentProps) {\n  \n  const [wikiInfo, setWikiInfo] = useState({\n    url: '',\n    title: '',\n    summary: '',\n    thumbnail: '',\n  });\n  \n  useEffect(() => {\n    (async() => {\n      const res = await getWikiSummary('en', title);\n      setWikiInfo(res);\n    })();\n  }, [ title ]);\n  return(\n    >\n      \/\/ conditional rendering\u3067\u901a\u5e38\u6642\u3068\u30a8\u30e9\u30fc\u3092\u5206\u5c90\n      {wikiInfo.error ? (\n         \/\/ wikiInfo.error \uff08\u30a8\u30e9\u30fc\u30e1\u30c3\u30bb\u30fc\u30b8\uff09\u3092\u8868\u793a\u3059\u308b \n      ) : (\n        \/\/ \u30ab\u30fc\u30c9\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306bwiki\u306e\u60c5\u5831\u3092\u8868\u793a\n      )}\n    \/>\n  );\n}","language":"typescript","id":2}

動作例と照らし合わせて、wikipediaの記事情報がAPI経由で取得できていることを確認してみてください。

  • 記事サムネイル:wikiInfo.thumbnail
  • 記事タイトル:wikiInfo.title
  • 記事サマリー:wikiInfo.summary
  • 記事リンク:wikiInfo.url
再掲。取得されたwikipedia記事情報を表示するコンポーネントの見た目

Reactの実装方法は本題から外れますので詳細は省きますが、useEffect()で非同期関数をトリガーしたい時は、useEffect()内部で非同期関数自体を宣言しなければならないことに注意してください。↑では即時関数で実装しています。

例えば以下のコード例はエラーになります。

{"code":"\/\/ NG\nuseEffect( async() => {\n  const res = await getWikiSummary('en', title);\n  setWikiInfo(res);\n}, [title]);","language":"typescript","id":2,"filename":""}

React16〜18にかけて実装されたSuspenseを使用すると、非同期のデータフェッチを待つ処理をよりシンプルに書けるようです。
ただし、その場合WikiInfoコンポーネント自体の実装が大きく変わりますので注意してください。

以上で本記事の説明を終わります。お疲れ様でした。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


error: Content is protected !!