AxiosはPromiseベースでHTTPリクエストを扱うことのできるHTTPクライアントです。
TypeScriptで型安全にAxiosを使用するためのポイントをまとめました。
動作確認のために、React+TypeScriptのフロントから実際にHTTPリクエストを送り、取得したデータをカード型コンポーネントに表示しています。記事と合わせてご覧ください。
動作例 GitHub
完成版のコードを早く見たい人は上記の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 APIのWikiMedia 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
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コンポーネント自体の実装が大きく変わりますので注意してください。
以上で本記事の説明を終わります。お疲れ様でした。