
关于
将命令式 TypeScript 代码重构为 fp-ts 函数式模式的全面指南。
name: fp-refactor description: 将命令式TypeScript代码重构为fp-ts函数式模式的全面指南 risk: unknown source: community version: 1.0.0 author: fp-ts-skills tags:
- fp-ts
- refactoring
- functional-programming
- typescript
- migration
- either
- option
- task
- reader
将命令式代码重构为fp-ts
本技能提供将现有命令式TypeScript代码迁移到fp-ts函数式编程模式的全面模式和策略。
何时使用
- 你正在将现有命令式TypeScript代码库重构为fp-ts模式。
- 任务涉及将
try/catch、空值检查、回调、依赖注入或循环转换为函数式等价物。 - 你需要迁移指导和权衡分析,而不仅仅是孤立的fp-ts示例。
目录
- 将try-catch转换为Either/TaskEither
- 将空值检查转换为Option
- 将回调转换为Task
- 将基于类的DI转换为Reader
- 将命令式循环转换为函数式操作
- 将Promise链迁移到TaskEither
- 常见陷阱
- 渐进式采用策略
- 何时不应重构
1. 将try-catch转换为Either/TaskEither
try-catch的问题
传统try-catch块有几个问题:
- 错误处理是隐式的,容易遗忘
- 类型系统不跟踪哪些函数可能抛出异常
- 控制流是非线性的,更难推理
- 组合多个可能失败的操作很冗长
模式:同步try-catch转Either
之前(命令式)
function parseJSON(input: string): unknown {
try {
return JSON.parse(input);
} catch (error) {
throw new Error(`Invalid JSON: ${error}`);
}
}
function validateUser(data: unknown): User {
try {
if (!data || typeof data !== 'object') {
throw new Error('Data must be an object');
}
const obj = data as Record<string, unknown>;
if (typeof obj.name !== 'string') {
throw new Error('Name is required');
}
if (typeof obj.age !== 'number') {
throw new Error('Age must be a number');
}
return { name: obj.name, age: obj.age };
} catch (error) {
throw error;
}
}
// Usage with nested try-catch
function processUserInput(input: string): User | null {
try {
const data = parseJSON(input);
const user = validateUser(data);
return user;
} catch (error) {
console.error('Failed to process user:', error);
return null;
}
}
之后(fp-ts Either)
import * as E from 'fp-ts/Either';
import * as J from 'fp-ts/Json';
import { pipe } from 'fp-ts/function';
interface User {
name: string;
age: number;
}
// Use Json.parse which returns Either<Error, Json>
const parseJSON = (input: string): E.Either<Error, unknown> =>
pipe(
J.parse(input),
E.mapLeft((e) => new Error(`Invalid JSON: ${e}`))
);
// Validation returns Either, making errors explicit in types
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 });
};
// Compose with pipe and flatMap - errors propagate automatically
const processUserInput = (input: string): E.Either<Error, User> =>
pipe(
parseJSON(input),
E.flatMap(validateUser)
);
// Handle both cases explicitly
pipe(
processUserInput('{"name": "Alice", "age": 30}'),
E.match(
(error) => console.error('Failed to process user:', error.message),
(user) => console.log('User:', user)
)
);
逐步重构指南
- 识别错误类型:确定可能发生的错误并创建适当的错误类型
- 更改返回类型:从
T改为Either<E, T>,其中E是你的错误类型 - 替换throw语句:将
throw new Error(...)转换为E.left(new Error(...)) - 替换return语句:将
return value转换为E.right(value) - 移除try-catch块:它们不再需要
- 更新调用方:使用
pipe配合E.flatMap来链接操作
模式:异步try-catch转TaskEither
之前(命令式)
async function fetchUser(id: string): Promise<User> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
throw new Error(`Failed to fetch user: ${error}`);
}
}
之后(fp-ts TaskEither)
import * as TE from 'fp-ts/TaskEither';
import { pipe } from 'fp-ts/function';
const fetchUser = (id: string): TE.TaskEither<Error, User> =>
pipe(
TE.tryCatch(
() => fetch(`/api/users/${id}`),
(e) => new Error(`Network error: ${e}`)
),
TE.flatMap((response) =>
response.ok
? TE.tryCatch(() => response.json(), (e) => new Error(`Parse error: ${e}`))
: TE.left(new Error(`HTTP error: ${response.status}`))
)
);
2. 将空值检查转换为Option
模式:null/undefined检查转Option
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
// 之前
function findUser(id: string): User | undefined {
return users.find(u => u.id === id);
}
const name = findUser("1")?.name ?? "Unknown";
// 之后
const findUser = (id: string): O.Option<User> =>
O.fromNullable(users.find(u => u.id === id));
const name = pipe(
findUser("1"),
O.map(u => u.name),
O.getOrElse(() => "Unknown")
);
3. 将回调转换为Task
import * as T from 'fp-ts/Task';
// 之前
function delay(ms: number, callback: () => void) {
setTimeout(callback, ms);
}
// 之后
const delay = (ms: number): T.Task<void> => () =>
new Promise(resolve => setTimeout(resolve, ms));
4. 将基于类的DI转换为Reader
import * as R from 'fp-ts/Reader';
import { pipe } from 'fp-ts/function';
interface Deps {
logger: { log: (msg: string) => void };
db: { query: (sql: string) => Promise<unknown[]> };
}
// 之前:类注入
class UserService {
constructor(private deps: Deps) {}
getUser(id: string) { /* ... */ }
}
// 之后:Reader模式
const getUser = (id: string): R.Reader<Deps, Promise<User>> =>
(deps) => deps.db.query(`SELECT * FROM users WHERE id = '${id}'`)
.then(rows => rows[0] as User);
5. 将命令式循环转换为函数式操作
import * as A from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
// 之前
const results: string[] = [];
for (const item of items) {
if (item.active) {
results.push(item.name.toUpperCase());
}
}
// 之后
const results = pipe(
items,
A.filter(item => item.active),
A.map(item => item.name.toUpperCase())
);
7. 常见陷阱
- 不要混合范式:避免在fp-ts管道中间使用throw
- 不要过度包装:简单值不需要Option包装
- 注意性能:深度嵌套的pipe可能影响调试体验
- 类型推断:有时需要显式类型注解帮助TypeScript
8. 渐进式采用策略
- 从边界开始:先在API边界和I/O层采用TaskEither
- 逐模块迁移:一次迁移一个模块,保持接口兼容
- 使用适配器:在fp-ts和命令式代码之间创建桥接函数
- 团队培训:确保团队理解核心概念后再大规模迁移
9. 何时不应重构
- 代码简单且不太可能变更
- 团队不熟悉函数式模式
- 性能关键路径中额外抽象不可接受
- 现有代码已经清晰且经过良好测试
兼容工具
Claude CodeCursor
标签
前端开发