发布于 2026-01-06 3 阅读
0

fp-ts 和漂亮的 API 调用

fp-ts 和漂亮的 API 调用

上次我们访问 fp-ts 时,我们进行了并发 API 调用,但没有花时间进行错误处理,也没有遵循 DRY 原则(不要重复自己)。现在我们都更成熟了,是时候重新审视一下了。让我们添加一些优雅的错误处理机制,并改进代码。以下是我们上次的代码:

const getUser = pipe(
  httpGet('https://reqres.in/api/users?page=1'),
  TE.map(x => x.data),
  TE.chain((str) => pipe(
    users.decode(str), 
    E.mapLeft(err => new Error(String(err))), 
    TE.fromEither)
  )
);

const getAnswer = pipe(
  TE.right("tim"),
  TE.chain(ans => pipe(
    answer.decode({ans}), 
    E.mapLeft(err => new Error(String(err))), 
    TE.fromEither)
  )
)

唉,重复代码太多了。而且我们的错误信息也毫无用处。如果我们运行上面的代码,会得到Error: [object Object]……这是什么鬼?完全没用,就是这样。我们可以做得更好。首先,让我们把错误信息写得更易读。

import { failure } from 'io-ts/lib/PathReporter'

const getAnswer = pipe(
  TE.right("tim"),
  TE.chain(ans => pipe(
    answer.decode({ans}), 
    E.mapLeft(err => new Error(failure(err).join('\n'))), 
    TE.fromEither)
  )
)

io-ts PathReporter 的failure方法接受一个 s 数组并返回一个字符串。如果我们运行它,我们会得到一个更有帮助的结果。不错。ValidationErrorError: Invalid value "tim" supplied to : { ans: number }/ans: number

好的,接下来我们来看看如何消除这些严重的重复数据。

const decodeWith = <A>(decoder: t.Decoder<unknown, A>) =>
  flow(
    decoder.decode,
    E.mapLeft(errors => new Error(failure(errors).join('\n'))),
    TE.fromEither
  )

const getUser = pipe(
  httpGet('https://reqres.in/api/users?page=1'),
  TE.map(x => x.data),
  TE.chain(decodeWith(users))
);

const getAnswer = pipe(
  TE.right({ans: 42}),
  TE.chain(decodeWith(answer))
)

这样看起来好多了。decoder.decode它接受一个参数unknown并返回一个Either<Errors, A>值,这很完美。但getUser它仍然非常依赖于特定的 URL 和类型,这不太方便。再说一遍:

const getFromUrl = <A>(url:string, codec:t.Decoder<unknown, A>) => pipe(
  httpGet(url),
  TE.map(x => x.data),
  TE.chain(decodeWith(codec))
);

哦耶!现在我们可以发起任何我们想要的 API 调用,响应将根据我们的编解码器进行验证。我们甚至可以在TE.mapLeft调用后添加一个语句httpGet,以便对 axios 抛出的错误进行一些特殊处理。

让我们把所有内容整合起来。

import axios, { AxiosResponse } from 'axios'
import { flatten, map } from 'fp-ts/lib/Array'
import * as TE from 'fp-ts/lib/TaskEither'
import * as E from 'fp-ts/lib/Either'
import * as T from 'fp-ts/lib/Task'
import { sequenceT } from 'fp-ts/lib/Apply'
import { pipe } from 'fp-ts/lib/pipeable'
import { flow } from 'fp-ts/lib/function'
import { failure } from 'io-ts/lib/PathReporter'
import * as t from 'io-ts'

//create a schema to load our user data into
const users = t.type({
  data: t.array(t.type({
    first_name: t.string
  }))
});
type Users = t.TypeOf<typeof users>

//schema to hold the deepest of answers
const answer = t.type({
  ans: t.number
});

//Convert our api call to a TaskEither
const httpGet = (url:string) => TE.tryCatch<Error, AxiosResponse>(
  () => axios.get(url),
  reason => new Error(String(reason))
)

//function to decode an unknown into an A
const decodeWith = <A>(decoder: t.Decoder<unknown, A>) =>
  flow(
    decoder.decode,
    E.mapLeft(errors => new Error(failure(errors).join('\n'))),
    TE.fromEither
  )

//takes a url and a decoder and gives you back an Either<Error, A>
const getFromUrl = <A>(url:string, codec:t.Decoder<unknown, A>) => pipe(
  httpGet(url),
  TE.map(x => x.data),
  TE.chain(decodeWith(codec))
);

const getAnswer = pipe(
  TE.right({ans: 42}),
  TE.chain(decodeWith(answer))
)

const apiUrl = (page:number) => `https://reqres.in/api/users?page=${page}`

const smashUsersTogether = (users1:Users, users2:Users) =>
  pipe(flatten([users1.data, users2.data]), map(item => item.first_name))

const runProgram = pipe(
  sequenceT(TE.taskEither)(
    getAnswer, 
    getFromUrl(apiUrl(1), users), 
    getFromUrl(apiUrl(2), users)
  ),
  TE.fold(
    (errors) => T.of(errors.message),
    ([ans, users1, users2]) => T.of(
      smashUsersTogether(users1, users2).join(",") 
      + `\nThe answer was ${ans.ans} for all of you`),
  )
)();

runProgram.then(console.log)
George,Janet,Emma,Eve,Charles,Tracey,Michael,Lindsay,Tobias,Byron,George,Rachel
The answer was 42 for all of you

如果我们返回错误数据,例如:

const getAnswer = pipe(
  TE.right({ans: "tim"}),
  TE.chain(decodeWith(answer))
)

我们得到

Invalid value "tim" supplied to : { ans: number }/ans: number

真漂亮。有了这种模式,我们可以处理以下任何API 调用:

  • 可能出错
  • 返回需要验证的内容
  • 按顺序运行、并行运行或单独运行

我们完全有信心,所有极端情况都已涵盖。祝您打字安全!

文章来源:https://dev.to/gnomff_65/fp-ts-and-beautiful-api-calls-1f55