如何更优雅地用fp-ts的TaskEither<Error, Option<A>>处理HTTP响应?
使用fp-ts优化TaskEither转HTTP响应的逻辑
我见过很多用TaskEither实现HTTP请求、文件读取的示例,现在要模拟根据ID从数据库查询数据的场景,这个操作有三种可能的输出:
- 找到目标数据
- 未找到对应ID的数据
- 出现错误(比如数据库连接失败)
对应的类型定义是TaskEither<Error, Option<A>>,简写为:
type TaskEO<A> = TaskEither<Error, Option<A>>
需要把这三种场景映射成对应的HTTP响应:
- 找到数据:返回200状态码 + 数据payload
- 未找到:返回404状态码
- 出错:返回500状态码
我已经实现了基础的转换逻辑,初始代码如下:
import * as O from "fp-ts/Option"; import * as TE from "fp-ts/TaskEither"; import * as E from "fp-ts/Either"; import { pipe } from "fp-ts/function"; type TaskEO<A> = TE.TaskEither<Error, O.Option<A>>; const getGoodStuff = (id: string): TaskEO<string> => TE.of(O.some(`result for ${id}`)); const getBadStuff = (id: string): TaskEO<string> => TE.left(new Error(`failed fetching ${id}`)); const getEmptyStuff = (id: string): TaskEO<string> => TE.of(O.none); getGoodStuff("123")() .then((e) => pipe( e, E.fold( (error) => `500: Internal Server Error`, (stuff) => pipe( stuff, O.match( () => `404: Not Found Error`, (value) => `200: Yay we got: "${value}"` ) ) ) ) ) .then(console.log);
替换调用不同的get...Stuff函数可以验证逻辑,但我觉得还有更简洁的组合方式,想请教怎么优化这段代码?
补充:我已经精简了代码
我把转换逻辑提炼成了单独的函数,代码如下:
enum HttpResponseCode { OK = 200, NOT_FOUND = 404, INTERNAL_SERVER_ERROR = 500 } type HttpResponse = { code: HttpResponseCode; payload: unknown; } const toHttpResponse = <A>(e: E.Either<Error, O.Option<A>>): HttpResponse => E.fold( (error) => ({ code: HttpResponseCode.INTERNAL_SERVER_ERROR, payload: "Internal Server Error" }), O.match( () => ({ code: HttpResponseCode.NOT_FOUND, payload: "Resource not found" }), (value) => ({ code: HttpResponseCode.OK, payload: value }) ) )(e)
这个函数可以直接用在Express路由里,用法示例:
async (req, res) => { await findStuffById(req.params.stuffId)() .then(toHttpResponse) .then(({ code, payload }) => res.status(code).send(payload)) }
优化建议
1. 直接在TaskEither链上组合转换逻辑
避免手动调用()执行Task,利用fp-ts的TE.fold直接处理TaskEither,让逻辑更连贯:
import * as O from "fp-ts/Option"; import * as TE from "fp-ts/TaskEither"; import { pipe } from "fp-ts/function"; // 复用已定义的响应枚举和类型 enum HttpResponseCode { OK = 200, NOT_FOUND = 404, INTERNAL_SERVER_ERROR = 500 } type HttpResponse = { code: HttpResponseCode; payload: unknown; } // 把转换逻辑适配为TaskEither的处理函数 const toHttpResponseTask = <A>(te: TE.TaskEither<Error, O.Option<A>>): TE.Task<HttpResponse> => pipe( te, TE.fold( () => TE.of({ code: HttpResponseCode.INTERNAL_SERVER_ERROR, payload: "Internal Server Error" }), (option) => pipe( option, O.match( () => TE.of({ code: HttpResponseCode.NOT_FOUND, payload: "Resource not found" }), (value) => TE.of({ code: HttpResponseCode.OK, payload: value }) ) ) ) ) // Express路由简化用法 async (req, res) => { await pipe( findStuffById(req.params.stuffId), toHttpResponseTask )().then(({ code, payload }) => res.status(code).send(payload)) }
2. 用TE.mapOption做类型化错误处理
显式区分错误类型,让后续扩展更灵活,符合fp-ts的错误处理范式:
// 定义自定义错误类型 class NotFoundError extends Error { readonly _tag = "NotFoundError"; constructor() { super("Resource not found"); this.name = "NotFoundError"; } } class InternalError extends Error { readonly _tag = "InternalError"; constructor(message: string) { super(message); this.name = "InternalError"; } } // 将原TaskEither转换为带类型错误的版本 const toTypedError = <A>(te: TE.TaskEither<Error, O.Option<A>>): TE.TaskEither<InternalError | NotFoundError, A> => pipe( te, TE.mapLeft(err => new InternalError(err.message)), TE.mapOption(opt => opt.match(() => new NotFoundError(), O.some)) ) // 错误转HTTP响应 const errorToResponse = (err: InternalError | NotFoundError): HttpResponse => { switch(err._tag) { case "InternalError": return { code: HttpResponseCode.INTERNAL_SERVER_ERROR, payload: err.message }; case "NotFoundError": return { code: HttpResponseCode.NOT_FOUND, payload: err.message }; } } // 最终组合逻辑 const toHttpResponse = <A>(te: TE.TaskEither<Error, O.Option<A>>): TE.Task<HttpResponse> => pipe( te, toTypedError, TE.fold( errorToResponse, (value) => ({ code: HttpResponseCode.OK, payload: value }) ) )
3. 封装路由处理逻辑
把执行Task和响应的逻辑封装成函数,让路由处理器更简洁:
const handleDbRequest = <A>( te: TE.TaskEither<Error, O.Option<A>>, res: Express.Response ) => pipe( te, toHttpResponseTask, TE.map(({ code, payload }) => res.status(code).send(payload)) )() // 路由中直接调用 async (req, res) => { await handleDbRequest(findStuffById(req.params.stuffId), res) }
内容的提问来源于stack exchange,提问作者Daniel Gruszczyk
相关产品推荐
相关产品推荐

