Effect (TS 库):TypeScript 生态下的函数式编程新标准

2 阅读1分钟

目录 (Outline)


一、 错误处理的「暗礁」:为什么 try/catch 不够用了?

1. 历史局限

在传统的 TS 开发中,我们习惯使用 try/catch

async function getData() {
  try {
    const res = await fetch('/api');
    return await res.json();
  } catch (e) {
    // 这里的 e 是 any,丢失了错误类型!
    console.error(e);
  }
}

2. 痛点

  • 错误类型丢失catch 块中的错误无法被编译器追踪。
  • 依赖耦合:很难在不修改核心逻辑的情况下替换数据库或 API 客户端。
  • 并发难控:管理多个异步任务的超时、重试和竞态条件非常繁琐。

二、 Effect-TS:全栈 TypeScript 的「瑞士军刀」

Effect 是一个专为 TypeScript 设计的函数式编程库。

核心特性

  1. 显式错误类型:错误也是类型系统的一部分。
  2. 环境隔离:通过 Requirements 实现完美的依赖注入。
  3. 强大的并发模型:内置 Fiber 调度器,支持非阻塞并发。
  4. 可组合性:所有操作都是纯函数,易于测试和复用。

三、 核心概念:Effect<Success, Error, Requirements>

一个 Effect 可以看作是一个「待执行的任务」,它有三个泛型参数:

  • Success:任务成功后返回的数据类型。
  • Error:任务可能抛出的错误类型(不再是 any!)。
  • Requirements:任务执行所需的依赖环境。

四、 快速上手:构建一个类型安全的 API 调用逻辑

代码示例

import { Effect, Console } from "effect"

// 定义一个可能失败的任务
const getUser = (id: string) => 
  Effect.tryPromise({
    try: () => fetch(`/api/user/${id}`).then(res => res.json()),
    catch: () => new Error("Network Error") // 明确错误类型
  })

// 组合逻辑
const program = getUser("1").pipe(
  Effect.map(user => `Hello, ${user.name}`),
  Effect.flatMap(msg => Console.log(msg))
)

// 执行
Effect.runPromise(program)

五、 实战 1:利用 Layers 实现声明式依赖注入 (DI)

在 Effect 中,你不需要手动传递 dbapiClient

实现步骤

  1. 定义 Tagclass Database extends Context.Tag("Database")<Database, { query: ... }>() {}
  2. 编写逻辑:在 Effect 中使用 yield* Database 获取依赖。
  3. 注入 Layer:在主程序运行前,通过 provide 注入真实的实现。

这种方式让你可以轻松地在测试环境中注入 MockDatabase,而在生产环境注入 PostgresDatabase


六、 实战 2:并发控制与超时处理——一行代码搞定

场景

并发获取 5 个用户的数据,如果某个请求超过 2 秒则超时,且整体失败时自动重试 3 次。

Effect 实现

const program = Effect.all(
  userIds.map(getUser), 
  { concurrency: 5 } // 并发数为 5
).pipe(
  Effect.timeout("2 seconds"),
  Effect.retry({ times: 3 })
)

对比:用传统的 Promise.all 配合 setTimeout 实现这套逻辑至少需要 30 行以上代码,且极易出错。


七、 总结:Effect-TS 带来的架构思维革命

Effect 不仅仅是一个库,它更像是一种全栈架构方案。它借鉴了 Scala (ZIO) 的思想,将函数式编程的严谨性带入了 TypeScript 生态。虽然学习曲线较陡,但对于构建高复杂度、高可靠性的系统来说,Effect 绝对是不二之选。