You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何更优雅地用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 19:23:30