使用TypeScript与fp-ts时Either返回类型的类型错误排查与解决
关于TypeScript+fp-ts注册表单验证的类型错误问题
问题描述
我正在用TypeScript和fp-ts重写旧应用,编写注册表单验证函数时碰到类型错误,搞不清问题根源,也不明白返回值为啥变成嵌套的Either类型。具体疑问:
- 为什么会出现这些类型错误?
- 怎么修复这些类型错误?
- 编码时该采取哪些措施尽量避免这类问题?
验证函数代码
const validateRegistrationForm = (user: User): Either<Error, User> => { return E.tryCatch( async () => { const schema = Joi.object().keys({ userName: Joi.string().min(4).max(255).required(), firstName: Joi.string().pattern(new RegExp(/^[a-z\d\-_\s]+$/i)).min(1).max(50).required(), lastName: Joi.string().pattern(new RegExp(/^[a-z\d\-_\s]+$/i)).min(1).max(50).required(), emailAddress: Joi.string().email().required(), password: Joi.alternatives().try(Joi.string(), Joi.number()).required(), confirmPassword: Joi.alternatives().try(Joi.string(), Joi.number()).disallow(Joi.ref('password')).required(), profilePicUrl: Joi.string().uri({scheme: [/https?/]}) }); let {error} = await schema.validateAsync(user); return error && error.length > 0 ? E.left({name: "ValidationError", message: error.message}) : E.right(user); }, (reason: Error) => E.left(new Error(String(reason))) ); }
错误信息
error TS2322: Type 'Either<Either<Error, never>, Promise<Right<User> | Left<{ name: string; message: any; }>>>' is not assignable to type 'Either<Error, User>'. Type 'Left<Either<Error, never>>' is not assignable to type 'Either<Error, User>'. Type 'Left<Either<Error, never>>' is not assignable to type 'Left<Error>'. Type 'Either<Error, never>' is not assignable to type 'Error'. Type 'Left<Error>' is missing the following properties from type 'Error': name, message return E.tryCatch(
解答
1. 类型错误的原因
- 异步逻辑与Either的误用:
E.tryCatch用于同步错误捕获,但你传入了async函数,异步函数默认返回Promise,再加上你在函数内部返回Either,导致最终返回Promise<Either<...>>,被E.tryCatch包装后形成嵌套的Either<Error, Promise<Either<...>>>,和声明的返回类型不匹配。 - 错误处理函数返回值错误:
E.tryCatch的第二个参数需要直接返回错误类型(比如Error),但你返回了E.left(new Error(...)),把错误也包装成了Either,导致外层Either的Left类型变成Either<Error, never>,不符合函数声明的Left<Error>要求。 - Joi验证逻辑错误:
schema.validateAsync验证失败时会直接抛出错误,而非返回带error属性的对象,所以let {error} = await schema.validateAsync(user)的写法本身就不成立,验证失败的情况不会走到你写的E.left分支,而是直接进入E.tryCatch的错误处理逻辑。
2. 修复方案
调整后的代码如下:
import * as TE from 'fp-ts/TaskEither'; import Joi from 'joi'; interface User { userName: string; firstName: string; lastName: string; emailAddress: string; password: string | number; confirmPassword: string | number; profilePicUrl?: string; } const validateRegistrationForm = (user: User): TE.TaskEither<Error, User> => { const schema = Joi.object().keys({ userName: Joi.string().min(4).max(255).required(), firstName: Joi.string().pattern(new RegExp(/^[a-z\d\-_\s]+$/i)).min(1).max(50).required(), lastName: Joi.string().pattern(new RegExp(/^[a-z\d\-_\s]+$/i)).min(1).max(50).required(), emailAddress: Joi.string().email().required(), password: Joi.alternatives().try(Joi.string(), Joi.number()).required(), // 修正:确认密码应该与密码相同,原代码disallow逻辑相反 confirmPassword: Joi.alternatives().try(Joi.string(), Joi.number()).valid(Joi.ref('password')).required(), profilePicUrl: Joi.string().uri({scheme: [/https?/]}) }); return TE.tryCatch( async () => { // validateAsync验证通过返回用户对象,失败直接抛错 return await schema.validateAsync(user, { abortEarly: false }); }, (reason) => new Error( reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '验证失败' ) ); };
关键修复点:
- 改用TaskEither处理异步场景:fp-ts中
TaskEither<L, R>专门用于异步且可能出错的逻辑,等价于() => Promise<Either<L, R>>,完美匹配异步验证的返回类型。 - 修正错误处理函数:直接返回
Error实例,而非E.left(Error),符合TE.tryCatch的参数类型要求。 - 修正Joi验证逻辑:移除错误判断分支,依赖
validateAsync的抛错机制进入错误处理;同时修正confirmPassword的规则,原代码的disallow与确认密码的需求完全相反,改为valid(Joi.ref('password'))。 - 调整函数返回类型:从
Either<Error, User>改为TE.TaskEither<Error, User>,匹配异步逻辑的实际返回值。
3. 避免此类问题的措施
- 严格遵循fp-ts类型职责:同步错误用
Either,异步错误用TaskEither,不要混用同步/异步类型的返回值。 - 吃透第三方库API:比如Joi的
validateAsync的行为是验证通过返回数据、失败抛错,提前查清楚API的返回/抛出规则,避免逻辑错误。 - 重视TypeScript类型提示:编写代码时关注编辑器的类型警告,不要忽略类型不匹配的提示,尽早排查问题。
- 用pipe组合函数:fp-ts中推荐用
pipe串联函数调用,避免嵌套调用导致的类型混乱,同时让代码更易读。 - 拆分复杂逻辑:把验证规则、错误格式化等拆成独立小函数,降低类型推断的复杂度,也更容易定位问题。
内容的提问来源于stack exchange,提问作者user1790300
相关产品推荐
相关产品推荐

