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

如何用fp-ts将TaskOption.Some转为TaskEither.Left实现用户创建校验

如何结合fp-ts实现用户创建时的邮箱唯一性校验

需求背景

需要实现一个用户创建函数,包含三个核心步骤:

  • 检查用户邮箱是否未被注册
  • 根据给定数据创建用户
  • 向新用户发送邮件

现有基础函数定义:

createOne: ({ password, ...creatableUser }: CreatableUser): taskEither.TaskEither<ExceptionError, User> => {
  ....
}

问题场景

尝试编写的代码如下,但无法实现当findByEmail返回TaskOption.Some(即用户已存在)时,返回TaskEither.Left错误的逻辑:

// * 疑问函数 
export const createOne = ({ password, ...creatableUserData }: CreatableUser): taskEither.TaskEither<Error, User> => {
  // 生成新用户
  const newUser: User = {
    ...creatableUserData,
    id: randomUUID(),
  };

  return pipe(
    // * 检查用户是否尚未存在
    creatableUserData.email,
    findByEmail,
    taskEither.fromTaskOption(() => new Error('User already exists')), // TODO: 需要在`Some`时返回错误
    // * 将新`User`保存到仓库
    taskEither.chain(() => save(newUser, password)),
    // * 向用户发送确认邮件
    taskEither.chain(() =>
      sendMail({
        body: 'Welcome to App Team',
      }),
    ),
    taskEither.map(() => newUser),
  );
};

相关类型定义

import { randomUUID } from 'crypto';
import { TaskOption } from 'fp-ts/lib/TaskOption';
import { taskEither } from 'fp-ts';
import { TaskEither } from 'fp-ts/lib/TaskEither';
import { pipe } from 'fp-ts/lib/function';

// * 上下文类型
type CreatableUser = {
  password: string;
  email: string;
  //...
}
type User = {
  email: string;
  id: string;
  //...
}

declare const findByEmail: (email: string) => TaskOption<User>;
declare const save: (user: User, password: string) => TaskEither<Error, void>;
declare const sendMail: (message: {body: string}) => TaskEither<Error, void>;

需要实现:当用户已存在时,通过过滤或预测函数提前返回错误。


更新:已实现的解决方案

目前通过以下代码解决了问题,但仍有优化空间:

export const makeCreateOne =
  (usersRepository: UsersRepository, mailProvider: MailProvider) =>
  ({ password, ...creatableUserData }: CreatableUser): TaskEither<ExceptionError, User> => {
    // 生成新用户
    const newUser: User = {
      ...creatableUserData,
      id: randomUUID(),
    };


    return pipe(
      // * 根据给定邮箱获取用户
      creatableUserData.email,
      usersRepository.findByEmail,
      // * 如果是`Some`则返回`null`
      TO.match(
        // None: 用户不存在,可以创建新用户
        () => true,
        // Some: 用户已存在,终止创建流程
        () => null,
      ),
      TE.fromNullable(createExceptionError('User already exists', REQUEST_STATUS.not_found)),
      // * 将新`User`保存到仓库
      TE.chain(() => usersRepository.save(newUser, password)),
      // * 向用户发送确认邮件
      TE.chain(() =>
        mailProvider.sendMail({
          body: 'Welcome to App Team',
        }),
      ),
      TE.map(() => newUser),
    );
  };

优化建议

方案1:使用taskOption.fold直接转换为TaskEither

不需要先转成true/null再用fromNullable,可以直接通过fold处理两种分支逻辑,代码更直观:

export const makeCreateOne =
  (usersRepository: UsersRepository, mailProvider: MailProvider) =>
  ({ password, ...creatableUserData }: CreatableUser): TaskEither<ExceptionError, User> => {
    const newUser: User = {
      ...creatableUserData,
      id: randomUUID(),
    };

    return pipe(
      creatableUserData.email,
      usersRepository.findByEmail,
      // 直接通过fold将TaskOption转为TaskEither
      TO.fold(
        // None: 用户不存在,继续流程,返回成功的空值
        () => TE.right(void 0),
        // Some: 用户已存在,直接返回错误
        () => TE.left(createExceptionError('User already exists', REQUEST_STATUS.not_found))
      ),
      TE.chain(() => usersRepository.save(newUser, password)),
      TE.chain(() => mailProvider.sendMail({ body: 'Welcome to App Team' })),
      TE.map(() => newUser)
    );
  };

方案2:封装邮箱唯一性校验的复用函数

如果这个校验逻辑会被多处使用,可以单独封装成函数,提高代码复用性和可读性:

const ensureEmailNotExists = (email: string, repo: UsersRepository): TaskEither<ExceptionError, void> =>
  pipe(
    email,
    repo.findByEmail,
    TO.fold(
      () => TE.right(void 0),
      () => TE.left(createExceptionError('User already exists', REQUEST_STATUS.not_found))
    )
  );

export const makeCreateOne =
  (usersRepository: UsersRepository, mailProvider: MailProvider) =>
  ({ password, ...creatableUserData }: CreatableUser): TaskEither<ExceptionError, User> => {
    const newUser: User = {
      ...creatableUserData,
      id: randomUUID(),
    };

    return pipe(
      ensureEmailNotExists(creatableUserData.email, usersRepository),
      TE.chain(() => usersRepository.save(newUser, password)),
      TE.chain(() => mailProvider.sendMail({ body: 'Welcome to App Team' })),
      TE.map(() => newUser)
    );
  };

方案3:避免提前创建newUser(可选优化)

如果校验不通过时不想提前生成newUser(比如避免不必要的randomUUID调用),可以把newUser的创建放到校验通过后的流程里:

export const makeCreateOne =
  (usersRepository: UsersRepository, mailProvider: MailProvider) =>
  ({ password, ...creatableUserData }: CreatableUser): TaskEither<ExceptionError, User> =>
    pipe(
      creatableUserData.email,
      usersRepository.findByEmail,
      TO.fold(
        () => TE.right(void 0),
        () => TE.left(createExceptionError('User already exists', REQUEST_STATUS.not_found))
      ),
      // 校验通过后再生成新用户
      TE.map(() => ({
        ...creatableUserData,
        id: randomUUID(),
      })),
      TE.chain(newUser => 
        pipe(
          usersRepository.save(newUser, password),
          TE.map(() => newUser)
        )
      ),
      TE.chain(newUser => 
        pipe(
          mailProvider.sendMail({ body: 'Welcome to App Team' }),
          TE.map(() => newUser)
        )
      )
    );

内容的提问来源于stack exchange,提问作者Puszkarek

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 21:50:15