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