LoginSignup
169
110

More than 3 years have passed since last update.

HTTPリクエストを型安全にする手法とOSS

Last updated at Posted at 2019-12-04

気付いた頃には全部埋まってた本命のTypeScriptアドベントカレンダーの初日が投稿されないようなので代わりに投稿させてもらうことにしました!

エンドポイントに文字列を使い続ける限りHTTPリクエストは型安全に出来ない

axios, ky, fetch, request, superagent...
どれを使っても「エンドポイントの指定が文字列」なせいで型安全にならない・・・
という課題にTypeScriptユーザーなら誰もが共感してくれるのではないかなーと思ってます
(TypeScript 4.1 の template string types である程度は改善しそうです)

例えば、axiosで以下のようなリクエストをするとして、

const userId = 1
const res = await axios.get<History>(`/user/${userId}/histories`)
console.log(res.data)

VSCodeでres.dataの返り値の型を見ると
無題.png
Historyなんだなあということがわかります
型が表示されてるので一見問題なさそうですが、

_人人人人人人人人人_
> 突然の仕様変更 <
 ̄Y^Y^Y^Y^Y^Y^Y^ ̄

  • エンドポイントのuserusers
  • 返り値がオブジェクトから配列に

なんてことはまあよくあるわけです
API叩いてるファイルで/user/とかで文字列検索して一つずつ書き換えていくことになります

const userId = 1
const res = await axios.get<History[]>(`/users/${userId}/histories`)
console.log(res.data)

返り値も配列に変わりました
(これがやりたくて無理やりhistoriesを登場させました・・・)
無題.png
しかし

const response = await axios.get<History>(`user/${userId}/histories`)

のように/user/にマッチしない書き方をしている行があって修正漏れ・・・
なんてことにならないように血眼になってファイルを確認する羽目になるわけです

これはTypeScriptだけでなく、静的型付け言語全てにおいて未解決の問題ではないでしょうか
(TypeScript以外の型付け言語を触ったことがないので想像です・・・)
エンドポイントを文字列で指定し続ける限り、HTTPリクエストは型安全にならないのです

エンドポイントをプロパティで指定出来れば型安全になる

'/users/1/histories'という文字列から型を判別させることは出来ないけど、
もしapi().users._userId(1).historiesのようにプロパティを繋いでリクエストできれば型安全に出来そうじゃないですか?
イメージはまさにこんな感じ
aspida.gif

VSCodeの補完が効いてタイポすることも無くなります
仕様変更があっても静的に型エラーを検出できるので安心ですね!
残る問題は型定義をどうやってラクに書くか・・・

エンドポイントの定義はディレクトリ・ファイル名で管理できるとラク

api().users._userId(1).historiesの型定義を
api/users/_userId/histories/index.tsファイルに書けたら直感的で楽しそう!

apis/users/_userId/histories/index.ts
type History = {
  id: number
  date: string
  action: 'walking' | 'sleeping'
}

export type Methods = {
  get: {
    resBody: History[] // GETの返り値はHistoryの配列
  }
}

こういうファイルを作ったらエンドポイント型定義がいい感じに自動で作成されて

const userId = 1
const res = await api().users._userId(userId).histories.get()
console.log(res.body)

こう書けるようになり、
さらにVSCodeで返り値の型を確認すると
無題.png
こうなってくれれば最高ですね!

つまりはapi/users/_userId/histories/index.tsファイルからエンドポイント型定義を生成するバッチを作ればいい・・・
けどこれが超めんどい!

一番めんどいところをやってくれるCLIをOSSとして公開しました

api/users/_userId/histories/index.tsファイルを作ればオートルーティング的にエンドポイント解決してくれていい感じの型定義ファイルを生成するTypeScript製CLI「aspida(アスピーダ)」を作りました
上記のアプローチで型安全なHTTPリクエストが出来るようになります
GitHub: aspida日本語ドキュメント

インストール

HTTPクライアントをaxios/ky/fetchから選ぶ必要があります
今回はaxiosのケースを紹介します

$ npm install @aspida/axios axios

オートルーティング対象のディレクトリを作る

$ mkdir api

apiディレクトリの直下からオートルーティング対象になります

型定義

上記で紹介した内容そのままです

api/users/_userId/histories/index.ts
type History = {
  id: number
  date: string
  action: 'walking' | 'sleeping'
}

export type Methods = {
  get: {
    resBody: History[] // GETの返り値はHistoryの配列
  }
}

ビルドしてルーティング解決されたTSファイルを生成

package.json
{
  "scripts": {
    "api:build": "aspida"
  }
}
$ npm run api:build

> api/$api.ts was built successfully.

apiディレクトリ直下に$api.tsというファイルが自動生成されます

アプリケーションからHTTPリクエストする

index.ts
import aspida from '@aspida/axios'
import api from "./api/$api"
;(async () => {
  const client = api(aspida())
  const userId = 1

  const res = await client.users._userId(userId).histories.get()
  console.log(res.body)

  // メソッドに$を付けるとレスポンス内容のみを返す
  const histories = await client.users._userId(userId).histories.$get()
  console.log(histories)
})()

フロント・バック問わずaxiosを使える環境ならどこでも動作します
一度型定義するだけでわざわざラッパーを作らずとも安全にHTTPリクエストが出来ます
baseURLやヘッダーにtokenを設定する方法はドキュメントを読んでください
「これはスゴい!」と思ったらGitHubにスターを押してもらえると非常に嬉しいです!
GitHub: aspida日本語ドキュメント

aspidaの意味

ギリシャ語で「盾」です
型安全の世界観と合っていてカッコいいなと思って名付けました!
(ちなみにaxiosはギリシャ語で「価値が有る」らしい)

logo.png

GitHubのスターを押してもらえると嬉しいです

aspidaは2019年10月から実戦投入しており、ほとんどのユースケースに対応できるレベルに達しています
ユーザーから報告があったバグも数時間以内に修正してリリース対応してきました
日本語英語両方でドキュメント整備も頑張っているのでGitHubのスターを押してもらえると非常に嬉しいです!
GitHub: aspida日本語ドキュメント

169
110
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
169
110