Help us understand the problem. What is going on with this article?

Fetch APIで取得したjsonを任意の型で取得する

More than 1 year has passed since last update.

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)で指定できるようにします。

fetcher.ts
// 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を上記の関数でラップするだけです。

fetcher.ts
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させています。良ければ参考にしてみてください。

markey
Reactを書くお仕事をしています。TypeScript教に入信。
https://note.mu/mar_key
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした