はじめに
この記事は 弁護士ドットコム Advent Calendar 2023 の 23 日目の記事です。
前日は @komtaki さんの「型パズルを理解しTypeScript中級者になる8のポイント」でした。
こんにちは、クラウドサイン事業本部の篠田(@tttttt_621_s)です。 普段はクラウドサインのフロントエンドの改善活動を行なっています。
私が普段携わっているプロダクトでは、HTTP クライアントライブラリとして Axios を使っています。
バージョンとしては 0.21.1 を使っており、アップデートしていきたいと考え直近のバージョンの Breaking changes を確認していました。
その際に、request method の引数の型定義が変更されていることに気づきました。この記事では、その際に調べたことや考えたことをまとめていきます。
以降便宜上、get
と post
のみを扱います。
現時点での型定義
まず現在 (0.21.1) の型定義を確認します。
export interface AxiosInstance { get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; post<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>; }
引用:https://github.com/axios/axios/blob/a64050a6cfbcc708a55a7dc8030d85b1c78cdf38/index.d.ts#L130-L147
T
と R
2 つの型引数を受け取るようになっており、デフォルトで T
は any
、R
は AxiosResponse<T>
となっています。
このことから例えば以下のように get
メソッドのレスポンスに型を渡すことができます。
(デモとして JSONPlaceholder を使用)
GET
:/albums/1
のレスポンスは以下のようになっています。
{ "userId": 1, "id": 1, "title": "quidem molestiae enim" }
import axios from 'axios' export type ApiFetchAlbumResponse = { userId: number id: number title: string } const { data } = await axios.get<ApiFetchAlbumResponse>( 'https://jsonplaceholder.typicode.com/albums/1' ) console.log(data)
T
に型を渡さない場合は any
となり、型安全ではなくなってしまいます。
import axios from 'axios' const { data } = await axios.get( 'https://jsonplaceholder.typicode.com/albums/1' ) // data は any 型なので、url は存在しないがエラーにならない console.log(data.url)
この例のように T
に型を渡すことを必須とするために、以下のような Axios の request method を内包した関数を用意してるプロジェクトも多いのではないでしょうか。
const getFrom = async <T, R = AxiosResponse<T>>( url: string, config?: AxiosRequestConfig ) => await axios.get<T, R>(url, config) const { data } = await getFrom('https://jsonplaceholder.typicode.com/albums/1') // 'data' は 'unknown' 型です。という型エラーになる console.log(data.url)
0.22.0 での型定義
0.22.0 の型定義を確認します。
export class Axios { get<T = never, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig<T>): Promise<R>; post<T = never, R = AxiosResponse<T>>(url: string, data?: T, config?: AxiosRequestConfig<T>): Promise<R>; }
引用:https://github.com/axios/axios/blob/72f14ceef7dae917057f1d5c221713610a65217b/index.d.ts#L138-L154
T
がデフォルトで never
になり、さらに data?
が T
となっています。
まず、T
がデフォルトで never
になった背景としては、それ以前のバージョンで T
の型を指定し忘れた場合、any
となり型安全性がなくなってしまうという問題に対処するためだと考えられます。
ここで never
よりも unknown
のほうが相応しいのではと考える人もいるでしょう。
しかし、 unknown
は TypeScript 3.0 で追加された型のため、0.22.0 実装当時は never
を使用したようです。
参考:Make the default type of response data never
次に data?
が T
となることで、困るパターンがあります。具体的にはリクエストボディとして送信するデータとレスポンスの型が異なる場合です。
以下に例を示します(デモとして JSONPlaceholder を使用)。
POST
:/posts
のレスポンスは以下のようになっています。
{ "userId": 10, "id": 101, "title": "テストタイトル", "body": "テストボディ" }
リクエストボディとして送信するデータは以下です。
{ "userId": 10, "title": "テストタイトル", "body": "テストボディ" }
import axios from 'axios' type ApiCreatePostResponse = { userId: number id: number title: string body: string } const { data } = await axios.post<ApiCreatePostResponse>( 'https://jsonplaceholder.typicode.com/posts', { // id が無いと型エラーとなる userId: 10, title: 'テストタイトル', body: 'テストボディ' } )
この挙動はバグとして報告され後のバージョンで修正されることになります。
0.23.0 での型定義
0.23.0 の型定義を確認します。
export class Axios { get<T = unknown, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>; post<T = unknown, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>; }
引用:https://github.com/axios/axios/blob/1025d1231a7747503188459dd5a6d1effdcea928/index.d.ts#L169-L185
data?
の型は 3 つ目の型引数 D
として受け取るようになっています。また T
がデフォルトで unknown
になっています。
まず、0.22.0 での T
を nerver
にして型安全性を保つという回避策が修正され、 T
が unknown
になりました。
参考:Change never type to unknown
次に新たに追加された型引数 D
ですが、こちらは 0.22.0 でデータとレスポンスの型が異なる場合エラーとなる事象の修正のために追加されたものです。
これによって以下のようにリクエストボディとして送信するデータとレスポンスの型が異なる場合でもエラーにならなくなりました。
import axios, { AxiosResponse } from 'axios' type ApiCreatePostResponse = { userId: number id: number title: string body: string } type ApiCreatePostData = { userId: number title: string body: string } const { data } = await axios.post< ApiCreatePostResponse, AxiosResponse<ApiCreatePostResponse>, ApiCreatePostData >('https://jsonplaceholder.typicode.com/posts', { userId: 10, title: 'テストタイトル', body: 'テストボディ', })
参考:Distinguish request and response data types
R
の指定を省略したい場合は以下のような関数を用意してるプロジェクトも多いのではないでしょうか。
const post = async <T, D, R = AxiosResponse<T>>( url: string, data?: D, config?: AxiosRequestConfig<D> ) => await axios.post<T, R, D>(url, data, config) const { data } = await post< ApiCreatePostResponse, ApiCreatePostData >('https://jsonplaceholder.typicode.com/posts', { userId: 10, title: 'テストタイトル', body: 'テストボディ', })
0.24.0 での型定義
0.24.0 の型定義を確認します。
export class Axios { get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>; post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>; }
引用:https://github.com/axios/axios/blob/53d6d37556a3443b00b3d9b4e7a934bf1d81aabe/index.d.ts#L169-L185
0.22.0 から明示的に T
に型を設定しなければなリませんでしたが、ここで any
に戻りました。
これについては多くの議論がされていましたが、any
とすることで多くの人にとって Axios が利用しやすくなるという結論になったようです。
参考:Revert #3002 - set type of data to any
まとめ
axios における各バージョンの request method の型定義を調査しました。
- 0.22.0:
T
がデフォルトでnever
になり、さらにdata?
がT
となっている - 0.23.0:
T
がデフォルトでunknown
になり、さらに 3 つ目の型引数D
を受けとりdata?
がD
となっている - 0.24.0:
T
がデフォルトでany
に戻った
おわりに
最後まで読んでいただきありがとうございました。
調査の結果から、まずは 0.24.0 にアップデートし、最終的には最新バージョンまでアップデートしていきたいと考えます。
今回 Axios の issue や PR を読んでいく中で、今まで当たり前のように使っていた便利なライブラリは多くの人の手によって作られていることを再認識しました。
来年は自分も何か OSS に貢献したいです。
明日は @NaokiTsuchiya さんの「ログや例外についてレビューや実装時に意識していること」です。
お楽しみに。