3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TypeScriptAdvent Calendar 2021

Day 7

fp-tsとio-tsを使ってWeb APIをいい感じに叩く

Last updated at Posted at 2021-12-07

はじめに

「fp-tsとかio-tsは使いこなせると便利だろうけどとっかかりがなぁ。。。」
「axios.getを叩いて返ってきたresponseの型をいい感じに推論したいなぁ。。。」
みたいな感情ないですか?僕はあります。

そういった方に向けて、fp-tsとio-tsを組み合わせたWebAPIの叩き方(with axios)の例を紹介します。
サンプルコードの実装はこちらのサイトを参考にしました。

この記事では扱わないこと

この記事では上記ライブラリに興味を持ってもらうことを目的としているため以下の内容は取り扱わないです

  • fp-tsio-tsの導入方法
  • fp-tsio-tsで提供されている機能の詳しい説明
  • 関数型言語について

環境

  • OS: Ubuntu 18.04.6 LTS
  • node: 12.22.5
  • fp-ts: 2.11.5
  • io-ts: 2.2.16

目次

  • responseの型定義を行う
    • WebAPIを叩いた時の返り値の型を考える
    • io-tsを用いて上記の型を実装する
    • io-tsを用いた型チェックのやり方を考える
  • response.dataの型チェックを行う
    • fp-tsを用いた関数合成について考える
    • fp-tsを用いて型チェック部分を実装する
  • axiosを用いてWebAPIを叩く
    • fp-tsを用いた関数のパイプラインについて考える
    • fp-tsを用いてWebAPIを叩く部分を実装する

responseの型定義を行う

というわけで早速やっていきます。
実際にaxiosを使ってWebAPIを叩く前に、返ってくるデータの型について考えます。

WebAPIを叩くと返ってくるデータの型について

サンプルとして、localhost:8080/ariticleを叩くと以下の様なArticle型のデータが返ってくるとします。

type Article = {
  title: string;
  discription: string;
  articleNumber: number;
};

io-tsを用いて型定義を行う

この部分では、型チェックを行うためにio-tsを用いた型定義及び型チェックのやり方について紹介します。

io-tsではcodecというものを定義して型チェックをすることが出来ます。
先程のArticle型をio-tsのcodecを用いて実装すると以下の様になります。

import * as t from "io-ts";

const ArticleCodec = t.type({
  title: t.string,
  discription: t.string,
  articleNumber: t.number,
})

// TypeScriptのtypeに変換可能
type Article = t.TypeOf<typeof ArticleCodec>;

上で定義したcodecはcodec.decode(型チェックしたい要素)で型チェックが可能です。

decodeの返り値の型はEither<Errors, T>です。
Either型はfp-tsで追加された型です。(ドキュメント)

decodeに成功するとEither型の右側の要素(今回だとT型)が、失敗すると左側の要素(今回だとErrors型)が返ってきます
ただし実際に返ってくる値の型はErrors | Tではなく、あくまでEither<Errors, T>である点に注意してください。

decodeのサンプル

const correctTypeData = {title: "タイトル", discription: "内容", articleNumber: 4};
console.log(ArticleCodec.decode(correctTypeData));
// { _tag: 'Right', right: {title: "タイトル", discription: "内容", articleNumber: 4} }

const wrongTypeData = {name: "なまえ"};
console.log(ArticleCodec.decode(wrongTypeData));
// { _tag: 'Left', left: ~~~ }

responseの型チェック部分の実装

この部分では、APIからresponseが返ってきた際に先程紹介したcodecを用いて型チェックを行う部分について考えます。
目標として以下の内容を満たすコードを挙げます。

  • 大体の型で型チェック出来る
  • 型チェック後のデータはその型に推論されている
  • 型チェックに失敗した時のエラーハンドリングが出来る

fp-tsを用いた関数合成について

この部分ではfp-tsが提供している便利関数その1であるflowと、TaskEither型を用いて実装します。
TaskEither型の説明は省略します(筆者自身の理解が浅く嘘を書きそうなので)
TaskEitherのドキュメント

flowについてです。
flowは簡単に説明すると、左から右への関数合成を行うものらしいです(by 公式)
なんのこっちゃ?なのでサンプルコードを用意しました。

flowのサンプル

import { flow } from 'fp-ts/function'

const len = (s: string): number => s.length
const double = (n: number): number => n * 2

const input = "hello";

// いつもの書き方
var tmp = len(input);
console.log(double(tmp));
// 10

// flowを使う場合
const f = flow(len, double);
console.log(f(input));
// 10

公式のサンプルをそのまま持ってきました。これを見れば関数合成のイメージがなんとなく掴めたと思います。

responseの型チェックを行う

では本題のaxiosからのreponseを受け取って型チェックを行う部分の実装を紹介します。

function decodeWith<T>(codec: t.Type<T>): (i: unknown) => TE.TaskEither<Error, T> {
  return flow(
    codec.decode,
    E.mapLeft(
      () => new Error("cannot decode response"),
    ),
    TE.fromEither,
  )
}

この部分では、データを受け取ってdecodeして結果を返すというよりかはそういった処理を行う関数群を生成して返すという感じになっています。

flow部分の説明としては、入力データをcodec.decodeしてdecodeに失敗した(結果がleftの時)場合はE.mapLeft( ~~~ )でdecodeの返り値であるEither<Errors, T>型のErrorsを返さずにError型を返すようにラップしてあげています。

またこの部分は関数群を生成して返すという性質上、呼び出し元が非同期で動いている場合はこの関数群も非同期で動作することを前提で実装する必要があります。なので返り値の型がEither型ではなくTaskEither型になっています(多分)

axiosを使ってrequestを発行する

この部分では、axiosを使ってger requestを投げ結果を受け取る部分について考えます。

目標として以下の内容を設定します。

  • requestを投げることが出来る
  • responseを受け取ることが出来る
  • 最低限のエラーハンドリングが出来る

fp-tsを用いた関数のパイプラインについて

この部分では先程も登場したTaskEither型を沢山使います。
またfp-tsが提供する便利関数その2であるpipeも使います。

pipeについてです。
先程紹介したflowと似ているやつだと思ってください。
pipeは式の値をパイプラインに繋いでいくというものです(by 公式)
相変わらずなんのこっちゃなのでサンプルを用意しました。

pipeのサンプル

import { pipe } from 'fp-ts/function'

const len = (s: string): number => s.length
const double = (n: number): number => n * 2

// without pipe
assert.strictEqual(double(len('aaa')), 6)

// with pipe
assert.strictEqual(pipe('aaa', len, double), 6)

公式のサンプルコードがとても分かりやすいのでそのまま持ってきました。
つまりそういうことです。
flowと違って渡したいデータも含める点が違います。(他にも色々違う点があると思いますが省略します)

axiosを使ってrequestを発行する

ということでaxiosをあれこれする部分です

import { pipe } from "fp-ts/function";
import axios from "axios";

function runHttpRequest<T>(codec: t.Type<T>, url: string): TE.TaskEither<Error, T> {
  return pipe(
    TE.tryCatch(
      () => axios.get(url),
      (error) => new Error(`${error}`),
    ),
    TE.map(response => response.data),
    TE.chain(decodeWith(codec)),
  )
}

runHttpRequest関数ではpipeを使うことによって以下の処理を順々に行います。

  • axiosを用いてget requestを発行する
  • responseからdataを取り出す
  • 先程紹介した型チェックを行う関数群(decodeWith関数)にdataを渡して結果を受け取る

この関数の返り値の型はTaskEitherとなっていますが、実際にはTaskEitherとなってます(多分)

実行する部分

最後にこれらの関数群を実行する部分を紹介します。

import * as T from "fp-ts/Task";

function main() {
  pipe(
    runHttpRequest(ArticleCodec, "localhost:8080/aritcle"),
    TE.fold(
      (error) => T.of(console.log(error)), // error: Error型
      (data) => T.of(console.log(data)), // data: Article型
    ),
  )
}

ここでは、pipeを用いてrequestの結果をTE.foldに流しています。
TE.foldでは流れてきたEither型(今回はTaskEither型)がleftの場合は(error) => ~~~を、rightの場合は(data) => ~~~を実行するようになっています。

全体像

import { pipe, flow } from "fp-ts/function";
import * as t from "io-ts";
import * as TE from "fp-ts/TaskEither";
import * as T from "fp-ts/Task";
import * as E from "fp-ts/Either";

/**
 * 受け取ったデータが指定された型に一致しているかどうか判断する
 * @param codec: 指定したい型
 * @returns 一致しているかどうかの結果
 */
function decodeWith<T>(codec: t.Type<T>): (i: unknown) => TE.TaskEither<Error, T> {
  return flow(
    codec.decode,
    E.mapLeft(
      () => new Error("cannot decode response"),
    ),
    TE.fromEither,
  )
}

/**
 * axiosを用いたget requestを発行しTaskEither型のオブジェクトを返す
 * @param codec: get responseのdataの型を指定
 * @param url: requestを投げる先
 * @returns: get requestの結果
 */
function runHttpRequest<T>(codec: t.Type<T>, url: string): TE.TaskEither<Error, T> {
  return pipe(
    TE.tryCatch(
      () => axios.get(url),
      (error) => new Error(`${error}`),
    ),
    TE.map(response => response.data),
    TE.chain(decodeWith(codec)),
  )
}


// 叩き方
const ArticleCodec = t.type({
  title: t.string,
  discription: t.string,
  articleNumber: t.number,
})

type Article = t.TypeOf<typeof ArticleCodec>

function main() {
  pipe(
    runHttpRequest(ArticleCodec, "localhost:8080/aritcle"),
    TE.fold(
      (error) => T.of(console.log(error)),
      (data) => T.of(console.log(data)),
    ),
  )
}
// リクエストを正常に投げられた場合
// 出力内容: { "記事のタイトル" "記事の内容" 記事の番号 }

// サーバー側の問題が発生した時
// 出力内容: 500 internal server error

// リクエストを正常に投げられたが、異なる型のデータが返ってきている場合
// 出力内容: cannnot decode response

最後に

こうやってfp-tsとio-tsを組み合わせることによってWebAPIをいい感じに叩くことが出来ます。
fp-tsとio-tsが組み合わさった時の強力なパワーを少しでも感じていただければ幸いです。

少しでも気になったそこのあなた
npm i fp-ts io-ts
で早速試してみましょう!!!

3
2
1

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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?