何时使用
- 正在把现有命令式 TypeScript 代码库逐步迁移到 fp-ts 函数式范式。
- 任务核心是把
try/catch、null 检查、回调、类式依赖注入(DI)、命令式循环、Promise链转换成函数式等价物。 - 你需要的是迁移策略与权衡取舍,而非孤立的 fp-ts 用法示例。
不该用(保持命令式更好):
- 简单同步纯函数(如
formatName),套pipe只增加复杂度无收益。 - 性能热点循环:fp-ts 的
A.map/A.filter会产生中间数组,处理百万级元素时保留for循环。 - 第三方库接口边界(Express 中间件等):只在边界处转换,内部保持库要求的形态。
- 团队不熟悉 fp-ts、或一次性 null 检查(
user?.name ?? 'x'已够),或错误类型无所谓(反正要 throw/log)的场景。 - 测试代码:以可读为先,不必函数式。
步骤 / 指令
通用迁移流程(以 try/catch → Either 为例,其它范式同构):
- 确定错误类型:列出可能发生的错误,定义错误类型(推荐带
_tag的联合类型)。 - 改返回类型:把
T改为Either<E, T>(异步用TaskEither<E, T>)。 - 替换
throw new Error(...)为E.left(...);替换return value为E.right(value)。 - 删除
try/catch块。 - 用
pipe+E.flatMap(异步TE.flatMap)串联各步,错误自动短路传播。 - 在系统边界用
E.match/O.match/getOrElse/toNullable与非 fp 代码对接。
null → Option:用 O.fromNullable 在边界包裹;返回类型 T | null 改 Option<T>;用 O.map/O.flatMap/O.filter 取代 if 判空。需要错误信息时 E.fromOption(() => err) 升级为 Either。
回调 → Task:优先改用 fs/promises 等 Promise API,再用 TE.tryCatch 包裹;多任务用 A.traverse(TE.ApplicativePar)(并行,遇错即停)或 TE.ApplicativeSeq(顺序)。
类 DI → Reader:把依赖收敛为 AppEnv 接口,服务函数返回 ReaderTaskEither<AppEnv, E, A>,用 RTE.ask 取环境、RTE.flatMapTaskEither 调依赖;运行时注入真实/测试 env:updateEmail(id, email)(env)()。
循环 → 函数式:for → A.map/A.filter/A.reduce,单遍聚合可用 A.foldMap(monoid);嵌套循环 → A.flatMap + A.uniq(Eq);while → 递归辅助函数或 RA.unfold。
Promise 链 → TaskEither:.then → TE.map/TE.flatMap,.catch → TE.orElse/TE.mapLeft,条件抛错 → TE.filterOrElse,Promise.all → A.traverse(TE.ApplicativePar)。
渐进采用策略:① 从边界(API/DB/文件/输入校验)起步;② 建桥接函数双向转换;③ 按模块迁移、先内部后公开 API;④ 类型驱动(先改签名让编译器标出所有调用点);⑤ 用测试当文档。
示例
同步 try/catch → Either(错误进类型,组合自动短路):
import * as E from 'fp-ts/Either';
import * as J from 'fp-ts/Json';
import { pipe } from 'fp-ts/function';
const parseJSON = (input: string): E.Either<Error, unknown> =>
pipe(J.parse(input), E.mapLeft((e) => new Error(`Invalid JSON: ${e}`)));
const validateUser = (data: unknown): E.Either<Error, User> => {
if (!data || typeof data !== 'object') return E.left(new Error('Data must be an object'));
const obj = data as Record<string, unknown>;
if (typeof obj.name !== 'string') return E.left(new Error('Name is required'));
if (typeof obj.age !== 'number') return E.left(new Error('Age must be a number'));
return E.right({ name: obj.name, age: obj.age });
};
const processUserInput = (input: string): E.Either<Error, User> =>
pipe(parseJSON(input), E.flatMap(validateUser));
异步编排用 TE.Do/TE.bind 累积上下文,执行时注意双重调用 await getUserWithPosts('123')():
const getUserWithPosts = (id: string): TE.TaskEither<Error, { user: User; posts: Post[] }> =>
pipe(TE.Do, TE.bind('user', () => fetchUser(id)), TE.bind('posts', () => fetchUserPosts(id)));
可复用桥接(命令式 ↔ fp-ts 之间过渡):
const tryCatchSync = <A>(f: () => A): E.Either<Error, A> =>
E.tryCatch(f, (e) => (e instanceof Error ? e : new Error(String(e))));
const fromPromise = <A>(p: Promise<A>): TE.TaskEither<Error, A> =>
TE.tryCatch(() => p, (e) => (e instanceof Error ? e : new Error(String(e))));
命令式 → fp-ts 速查表(节选):
| 命令式 | fp-ts 等价 |
|---|---|
try/catch |
E.tryCatch() / TE.tryCatch() |
throw / return |
E.left() / E.right()(TE 同理) |
if (x === null) / x ?? d |
O.fromNullable() / O.getOrElse() |
x?.prop |
O.map() / O.flatMap() |
array.map/filter/reduce/find |
A.map/filter/reduce(foldMap)/findFirst |
Promise.then/catch |
TE.map+flatMap / TE.orElse+mapLeft |
Promise.all |
A.traverse(TE.ApplicativePar) |
new Class(deps) |
R.asks() / RTE.ask() |
for...of / while |
A.map+reduce / 递归、unfold() |
注意事项
常见坑(源中 Pitfalls,务必规避):
- 忘记运行 Task:
fetchData()仍是 Task,结果需await fetchData()()双重调用。 - 不要中途逃出 fp-ts 生态:拿到
Either就E.isLeft判断后throw,应改用TE.fromEither留在管线内。 - 该用
flatMap却用map:返回 monad 的函数用map会得到嵌套Either<E, Either<E, A>>,用flatMap摊平。 - 丢失错误上下文:
TE.tryCatch第二参要保留reason((reason) => new Error(...${reason})),更佳用带_tag的类型化错误。 - 滥用
fromNullable:简单取值user?.name ?? 'x'即可,仅在多步链式时才用 Option。 - 务必处理 Left/None:禁止
(result as E.Right<User>).right之类不安全断言,用E.getOrElse/E.match。
迁移是过程而非目的:从小处改起、保持务实(并非一切都要函数式)、类型驱动、充分测试、沉淀团队模式、复核收益。目标是更可维护、类型安全的代码,而非为函数式而函数式。
互见
- 研发/misc 下其它 TypeScript / 函数式相关条目。
- fp-ts 官方模块文档:
Either/Option/Task/TaskEither/Reader/ReaderTaskEither/Array/Monoid。
采编自 sickn33/antigravity-awesome-skills(MIT)。