Effect:把副作用塞进类型系统的 TypeScript 框架

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)eunknown——你根本不知道会抛什么。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 味道重pipeEffect.genyield* 这套语法看着就劝退
  • API 大ScheduleLayerFiberContextRefStream 一堆概念
  • 教程少:中文资料尤其稀
  • 和主流库有摩擦:接第三方 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。把返回值、错误、依赖三件事都刻进类型里,代价是学习曲线陡。大型项目用它能治很多老毛病,小项目就别折腾。