エンドポイントに文字列を使い続ける限りHTTPリクエストは型安全に出来ない
axios, ky, fetch, request, superagent...
どれを使っても「エンドポイントの指定が文字列」なせいで型安全にならない・・・
という課題にTypeScriptユーザーなら誰もが共感してくれるのではないかなーと思ってます
例えば、axiosで以下のようなリクエストをするとして、
const userId = 1
const res = await axios.get<History>(`/user/${userId}/histories`)
console.log(res.data)
VSCodeでres.data
の返り値の型を見ると
History
なんだなあということがわかります
型が表示されてるので一見問題なさそうですが、
_人人人人人人人人人_
> 突然の仕様変更 <
 ̄Y^Y^Y^Y^Y^Y^Y^ ̄
- エンドポイントの
user
がusers
に - 返り値がオブジェクトから配列に
なんてことはまあよくあるわけです
API叩いてるファイルで/user/
とかで文字列検索して一つずつ書き換えていくことになります
const userId = 1
const res = await axios.get<History[]>(`/users/${userId}/histories`)
console.log(res.data)
返り値も配列に変わりました
(これがやりたくて無理やりhistoriesを登場させました・・・)
しかし
const response = await axios.get<History>(`user/${userId}/histories`)
のように/user/
にマッチしない書き方をしている行があって修正漏れ・・・
なんてことにならないように血眼になってファイルを確認する羽目になるわけです
これはTypeScriptだけでなく、静的型付け言語全てにおいて未解決の問題ではないでしょうか
(TypeScript以外の型付け言語を触ったことがないので想像です・・・)
エンドポイントを文字列で指定し続ける限り、HTTPリクエストは型安全にならないのです
エンドポイントをプロパティで指定出来れば型安全になる
'/users/1/histories'
という文字列から型を判別させることは出来ないけど、
もしapi().users._userId(1).histories
のようにプロパティを繋いでリクエストできれば型安全に出来そうじゃないですか?
イメージはまさにこんな感じ
VSCodeの補完が効いてタイポすることも無くなります
仕様変更があっても静的に型エラーを検出できるので安心ですね!
残る問題は型定義をどうやってラクに書くか・・・
エンドポイントの定義はNuxtのオートルーティングっぽく書けるとラク
個人的にNuxtのオートルーティング機能が気に入っています
これを応用してapi().users._userId(1).histories
の型定義を
apis/users/_userId/histories.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.data)
こう書けるようになり、
さらにVSCodeで返り値の型を確認すると
こうなってくれれば最高ですね!
つまりはapis/users/_userId/histories.ts
ファイルからエンドポイント型定義を生成するバッチを作ればいい・・・
けどこれが超めんどい!
一番めんどいところをやってくれるCLIをOSSとして公開しました
apis/users/_userId/histories.ts
ファイルを作ればオートルーティング的にエンドポイント解決してくれていい感じの型定義ファイルを生成するTypeScript製CLI「aspida(アスピーダ)」を作りました
上記のアプローチで型安全なHTTPリクエストが出来るようになります
GitHub: aspida日本語ドキュメント
インストール
$ npm install @aspida/axios axios
axiosに依存してます
オートルーティング対象のディレクトリを作る
$ mkdir apis
Nuxtでいうところのpages
みたいなイメージです
apisディレクトリの直下からオートルーティング対象になります
型定義
上記で紹介した内容そのままです
type History = {
id: number
date: string
action: 'walking' | 'sleeping'
}
export type Methods = {
get: {
resBody: History[] // GETの返り値はHistoryの配列
}
}
ビルドしてルーティング解決されたTSファイルを生成
{
"scripts": {
"api:build": "aspida --build"
}
}
$ npm run api:build
> apis/$api.ts was built successfully.
アプリケーションからHTTPリクエストする
import aspida from "@aspida/axios"
import api from "./apis/$api"
;(async () => {
const client = api(aspida())
const userId = 1
const res = await api().users._userId(userId).histories.get()
console.log(res.data)
// Nuxtのaxios moduleっぽくも書ける
const histories = await api().users._userId(userId).histories.$get()
console.log(histories)
})()
Vue/Nuxtに限らずaxiosを使える環境ならどこでも動作します
一度型定義するだけでわざわざラッパーを作らずとも安全にHTTPリクエストが出来ます
baseURLやヘッダーにtokenを設定する方法はドキュメントを読んでください
「これはスゴい!」と思ったらGitHubにスターを押してもらえると非常に嬉しいです!
GitHub: aspida日本語ドキュメント
Nuxt TypeSciptで実装してみよう
サンプルとしてNuxtのaxios moduleとaspidaを組み合わせる方法を紹介します
インストール
Nuxt TypeScriptのセットアップは完了している前提です
npm install @nuxtjs/axios @aspida/axios
npm install npm-run-all --save-dev
apiの型定義を行うディレクトリを作る
$ mkdir apis
apiの型定義ファイルを作る
type History = {
id: number
date: string
action: 'walking' | 'sleeping'
}
export type Methods = {
get: {
resBody: History[] // GETの返り値はHistoryの配列
}
}
プラグインに$apiを登録する
aspidaにaxios moduleを引数として渡した結果を$api
プラグインとしてinjectします
(この時点では'~/apis/$api'
ファイルが存在しないのでVSCode上でエラーが表示されます)
import { Plugin } from '@nuxt/types'
import axiosClient from '@aspida/axios'
import api, { ApiInstance } from '~/apis/$api'
declare module 'vue/types/vue' {
interface Vue {
$api: ApiInstance
}
}
const plugin: Plugin = ({ $axios }, inject) => inject('api', api(axiosClient($axios)))
export default plugin
nuxt.congif.ts
への追記も忘れずに
import { Configuration } from '@nuxt/types'
const config: Configuration = {
plugins: [
'~/plugins/api',
],
modules: [
'@nuxtjs/axios',
],
axios: {
baseURL: 'http://example.com/api/v1' // aspidaにもbaseURLが反映されます
}
}
export default config
グローバルの$nuxtから$apiを呼ぶ
globalに$nuxtの型定義を上書きします
import '@nuxtjs/axios'
import { NuxtApp } from '@nuxt/types/app'
declare global {
const $nuxt: NuxtApp
}
あらゆるファイルで以下のように$apiを呼び出せるようになります
const histories = await $nuxt.$api.users._userId(userId).histories.$get()
npm scriptに登録して開発・ビルドする
--watch
オプションでapisディレクトリの変更を検知してapis/$api.ts
を自動更新できます
開発時(dev)はnpm-run-all
で並列にaspidaとnuxt-tsを呼び、ビルド時(generate)はaspidaのあとにnuxt-tsを呼ぶようにすると良いです
{
"scripts": {
"dev": "npm-run-all --parallel dev:*",
"dev:nuxt": "nuxt-ts",
"dev:api": "aspida --watch"
"generate": "aspida --build && nuxt-ts generate"
}
}
以下のコマンドでブラウザに表示されたら成功です
$ npm run dev
世界中のWebAPIの型定義をnpmに登録出来たらもっとフロントが楽しくなる
内製のAPIは上記のように一人が型定義をしておけば他のメンバーが安心してHTTPリクエストを行えるようになります
一方でTwitter, GoogleMap, YouTubeなど一般公開されててよく使うAPIは、型定義を誰かが書いてnpmに登録しておいてくれたら世界中のみんながラクにAPIを使えるようになりそうじゃないですか!?
ということで@types/node
のノリで@aspida/twitter
でTwitter APIの型定義をインストールできるようになるDefinitelyTypedみたいな仕組みも絶賛開発中です
aspidaの意味
ギリシャ語で「盾」です
型安全の世界観と合っていてカッコいいなと思って名付けました!
(ちなみにaxiosはギリシャ語で「価値が有る」らしい)
GitHubのスターを押してもらえると嬉しいです
aspidaは2か月以上前から実戦投入しており、axiosの利用が可能な案件であればほとんどのユースケースに対応できるレベルに達しています
ユーザーから報告があったバグも数時間以内に修正してリリース対応してきました
日本語英語両方でドキュメント整備も頑張っているのでGitHubのスターを押してもらえると非常に嬉しいです!
GitHub: aspida日本語ドキュメント
最後まで読んでいただきありがとうございました
明日のアドベントカレンダー担当は@Yoshihiro-Hiroseさんです!
Vue入門者向けのVuex解説記事も公開しています
Vue.js + Vuexでデータが循環する全体像を図解してみた