弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

Axios における request method の引数の型定義を調査した話

はじめに

この記事は 弁護士ドットコム Advent Calendar 2023 の 23 日目の記事です。

前日は @komtaki さんの「型パズルを理解しTypeScript中級者になる8のポイント」でした。

こんにちは、クラウドサイン事業本部の篠田(@tttttt_621_s)です。 普段はクラウドサインのフロントエンドの改善活動を行なっています。

私が普段携わっているプロダクトでは、HTTP クライアントライブラリとして Axios を使っています。

バージョンとしては 0.21.1 を使っており、アップデートしていきたいと考え直近のバージョンの Breaking changes を確認していました。

その際に、request method の引数の型定義が変更されていることに気づきました。この記事では、その際に調べたことや考えたことをまとめていきます。

以降便宜上、getpost のみを扱います。

現時点での型定義

まず現在 (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

TR 2 つの型引数を受け取るようになっており、デフォルトで TanyRAxiosResponse<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: 'テストボディ'
  }
)

この挙動はバグとして報告され後のバージョンで修正されることになります。

参考:breaking interface change

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 での Tnerver にして型安全性を保つという回避策が修正され、 Tunknown になりました。

参考: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 さんの「ログや例外についてレビューや実装時に意識していること」です。

お楽しみに。