Effect 是最近一两年在 TS 圈里越来越火的一个函数式框架。目标是——用类型系统管理副作用,让异步、错误、依赖都变成显式约束。
如果你在写”复杂异步 + 到处 try/catch + 依赖注入乱得一团”的项目,Effect 是一个解法。如果只是写小工具、静态站,用它就是杀鸡用牛刀。
一眼看懂它在做什么
普通 async:
async function getUser(id: string) {
const res = await fetch(`/api/user/${id}`);
return res.json();
}
Effect 风格:
const getUser = (id: string) =>
Effect.gen(function* () {
const client = yield* HttpClient.HttpClient;
const res = yield* client.get(`/api/user/${id}`);
return yield* res.json;
});
多了很多东西——但类型签名会告诉你所有事:
Effect.Effect<User, HttpError | JsonError, HttpClient>
// ↑ ↑ ↑
// 成功值类型 可能的错误 需要注入的依赖
三元组:Effect<A, E, R>
| 泛型 | 含义 | 例子 |
|---|---|---|
A | 成功返回值 | User |
E | 可能的错误 | NotFoundError | DBError |
R | 依赖环境 | Database & UserRepo |
跟 Promise 只有 A(成功值)相比多了两维,这就是 Effect 的核心卖点:错误和依赖都在类型里,没处理就编译不过。
它擅长解决的问题
1. 类型安全的错误处理
普通 TS 的 catch (e) 里 e 是 unknown——你根本不知道会抛什么。Effect 里你必须显式列出错误类型:
class NotFoundError { readonly _tag = "NotFoundError"; }
class DBError { readonly _tag = "DBError"; constructor(readonly cause: unknown) {} }
const findUser = (id: string): Effect.Effect<User, NotFoundError | DBError, Database> =>
Effect.gen(function* () {
const db = yield* Database;
const row = yield* db.query(`SELECT ...`).pipe(
Effect.mapError(cause => new DBError({ cause }))
);
if (!row) return yield* Effect.fail(new NotFoundError());
return row;
});
处理错误用 catchTag 按 tag 分派:
findUser("123").pipe(
Effect.catchTag("NotFoundError", () => Effect.succeed(defaultUser)),
Effect.catchTag("DBError", err => Effect.log(`DB 挂了: ${err.cause}`)),
);
2. 依赖注入靠类型驱动
服务用 Context.Tag 声明:
class Database extends Context.Tag("Database")<Database, {
query: (sql: string) => Effect.Effect<Row[], DBError>;
}>() {}
用的时候 yield* Database 就注入进来。运行前必须提供实现,缺了就编译不过:
const program = findUser("1");
const runnable = program.pipe(
Effect.provideService(Database, realDatabaseImpl)
);
Effect.runPromise(runnable);
跟 NestJS / Angular 的 DI 类似,但类型是硬约束,不是运行时 assert。
3. 并发是一等公民
Effect.all([task1, task2, task3], { concurrency: 5 }); // 限并发
Effect.race([primary, fallback]); // 抢先
task.pipe(Effect.timeout("2 seconds")); // 超时
task.pipe(Effect.retry(Schedule.exponential("100 millis")));// 指数退避重试
底层是 Fiber(协程),比 Promise 强得多——真的支持取消传播。
4. Resource 自动管理
const withDb = Effect.acquireRelease(
openConnection, // 拿资源
conn => closeConnection(conn),// 释放资源(成功、失败、被取消都会调)
);
withDb.pipe(
Effect.andThen(conn => queryAll(conn))
);
再也不用记着写 try { ... } finally { conn.close() }。
为什么很多人学不动
不吹了,说实话——Effect 有几个门槛:
- FP 味道重:
pipe、Effect.gen、yield*这套语法看着就劝退 - API 大:
Schedule、Layer、Fiber、Context、Ref、Stream一堆概念 - 教程少:中文资料尤其稀
- 和主流库有摩擦:接第三方 Promise-based 库要用
Effect.tryPromise包一层
什么时候值得用
合适:
- 大型 Node.js 后端(几万行以上)
- 复杂业务逻辑,错误路径多
- 团队认可 FP 思路
- 长期维护
别用:
- 前端简单 CRUD
- 小工具 / CLI
- 团队不熟 FP,学习成本远大于收益
- 只是想装酷
最小可运行示例
npm i effect
import { Effect, Console } from "effect";
const program = Effect.gen(function* () {
yield* Console.log("start");
const n = yield* Effect.succeed(42);
yield* Console.log(`n = ${n}`);
return n;
});
Effect.runPromise(program).then(console.log);
跑起来:
start
n = 42
42
一句话总结
Effect = TypeScript 版 ZIO。把返回值、错误、依赖三件事都刻进类型里,代价是学习曲线陡。大型项目用它能治很多老毛病,小项目就别折腾。
