Fetch API使っていますでしょうか?便利な反面、取扱いが面倒な点もあります。
「そもそもFetch APIってなに?」って方はこちらをご参考ください。
https://qiita.com/tomoyukilabs/items/9b464c53450acc0b9574
Fetch APIの面倒な点
- せっかくTypeScriptを使っていても、レスポンスのjsonがany型になる
- いちいちresponse.okで出し分けをする等の処理が必要になる
そこで、fetchをそのまま使うのではなく、fetchをラップして上記の課題を解決した(レスポンスを型を付けつつ便利に安全に取り出す)ものを共通的に使い回すようにしたいというのが今回のモチベーションです。
なお、個人的にReactでSSRしているのでクライント・サーバー両方で使えるisomorphic-unfetchを使っていますが、普通のFetch APIでも同じだと思います。(たぶん。。違っていたら教えてください)
fetchをラップする
fetch(input: RequestInfo, init?: RequestInit)
のreturn typeはPromise<Response>
になるので、そいつをラップする関数を作ります。response.okで出し分けし、jsonを取得できた場合のみresolveとし、それ以外の全ての場合はrejectとします。こうすることで、このラップされたfetchを使用する側では、単純にthenかcatchでレスポンスの結果を振り分けることが可能になります。jsonの型は型引数(T)で指定できるようにします。
// Tはレスポンスのjsonの型を指定する
const wrap = <T>(task: Promise<Response>): Promise<T> => {
return new Promise((resolve, reject) => {
task
.then(response => {
if (response.ok) {
response
.json()
.then(json => {
// jsonが取得できた場合だけresolve
resolve(json)
})
.catch(error => {
reject(error)
})
} else {
reject(response)
}
})
.catch(error => {
reject(error)
})
})
}
あとは、本来のfetchを上記の関数でラップするだけです。
import fetch from 'isomorphic-unfetch'
// const wrap = ...
const fetcher = <T = any>(
input: RequestInfo,
init?: RequestInit
): Promise<T> => {
return wrap<T>(fetch(input, init))
}
export default fetcher
使用する側ではこうなります。例ではQiitaの記事を取得するAPIを叩いています。実際にVSCode等のエディタで動かしてみるとわかりますが、itemsの型がQiitaItems
になっていてArrayのメソッドであるforEachが使えていますし、その要素のitemの型もきちんと取得できているので、item.idやitem.titleといったコード補完も効きます。
import fetcher from './fetcher'
// レスポンスの型を定義
type QiitaItems = Array<{
id: string
title: string
url: string
}>
function getQiitaItems() {
// レスポンスの型を指定
fetcher<QiitaItems>(
`https://qiita.com/api/v2/authenticated_user/items?per_page=5`,
{
headers: {
Authorization: `Bearer ${process.env.QIITA_ACCESS_TOKEN}`
}
}
)
// item.idやitem.title、item.urlが参照できる
.then(items => items.forEach(item => console.log(item.id)))
.catch(err => console.log(err))
}
リポジトリ
記事を読んでいただきありがとうございました。
最後にリポジトリを載せておきます。
https://github.com/masaki-koide/my-portfolio/blob/master/src/app/api/fetcher.ts
ここでは更に、timeoutという関数でラップしてfetch通信のタイムアウトを設定できるようにしています。仕組みとしては、setTimeoutで指定ミリ秒数後にrejectするpromiseと本来のタスクを行うpromiseをPromise.raceさせています。良ければ参考にしてみてください。