[控场AI]
· 10 分钟阅读· 5,453 字

Effect V4 实战:AI时代如何真正学会一个TypeScript库

Effect V4 实战:AI时代如何真正学会一个TypeScript库

ThePrimeagen 直播学习 Effect V4,以亲手理解代码为验收线,揭示类型化错误、结构化并发与依赖注入如何让 AI 协作更安全。

在 AI 能自动生成代码的当下,知名开发者 ThePrimeagen 通过两小时直播学习 TypeScript 库 Effect V4,用「能看懂生成代码」作为学习验收标准,抵制「虚假进步幻觉」。Effect 的核心价值体现在三层:其一,用 `Effect<Success, Error, Requirements>` 类型强制把可能的错误写进类型系统,弥补 TypeScript 无法追踪 `throw` 的天然缺陷;其二,结构化并发内建于运行时,一个请求失败时其余自动中断,彻底省去手动 AbortController 布线;其三,依赖注入通过 Requirements 类型参数在编译时验证服务依赖,确保服务不会私自访问数据库,测试时无需复杂 mock。开发者 TJ 进一步给出实践建议:用 `Data.TaggedError` 定义具名错误、用 Schema 取代 interface。直播也呈现了社区对「重新发明 JS」的质疑,但「知道会发生哪些错误是有用的」这一点获得了共识。

在 AI 编程工具大行其道的当下,一个尖锐的问题浮出水面:既然模型能帮你写代码,为什么还要费力去学一个新库?知名开发者 ThePrimeagen 在一场直播学习流中,以 TypeScript 库 Effect(V4 beta 版本)为对象,边读文档边动手,用两个多小时给出了自己的答案。这场看似松散的学习过程,恰恰揭示了 Effect 的核心价值,也道出了 AI 时代学习方式的本质分歧。

为什么 AI 时代仍要亲手学库

直播一开始就有观众抛出质疑:面对 AI,你怎么还需要辩护式地去学一个库?Primeagen 的回答很直接——因为你仍然得知道怎么用 AI

他打了个精妙的比方:读文档时如果跳得太快、漏掉小概念,就像数学基础好的人快速略过基础,结果撞上一堵怎么也翻不过去的墙,因为积累了太多没搞懂的小细节。他刻意放慢节奏,边读边在脑子里猜测每个概念的含义,再和文档给出的解释对照。

值得关注的是他的一套「AI 生成代码验收标准」:让 AI(借助 TJ 的配置)生成了一个 Effect 项目骨架,但他反复强调「我不喜欢活在一个我不知道这东西怎么运作的世界里」。他的验收方式是——用眼睛看生成的代码,能看懂就算过关。他对着满屏不认识的 layerscopedprovideMainSentryconfig 直言:「我不知道这些是干嘛的,既然翻译看起来是对的,那我现在就得去学它到底什么意思。」

这构成了他反复对观众强调的观点:只看到东西往前动、却不理解原理,是「进步的虚假幻觉」(false illusion of progress),最终你只能作为一个 script kitty,被工具本身能走多远所限制。

Effect 的第一层价值:类型化的错误

Effect V4 的文档开门见山点出了 TypeScript 的老毛病:「类型化的数据,无类型的程序」。TypeScript 擅长描述数据,却几乎不告诉你程序会怎么失败——一个函数签名不会告诉你它可能抛出什么错误。这是个「永远存在」的 TypeScript issue,且很可能被标记为 closed as not planned。

Effect 的核心类型正是围绕这点设计:Effect<Success, Error, Requirements>,三个类型参数分别表示成功值、可能的错误、以及执行所需的上下文依赖。与 Promise 只暴露 resolved 值、把错误吞进 unknown 不同,Effect 强制你把错误写进类型系统。

Effect 项目通过 bun run index 执行

Primeagen 通过 kit langton 制作的交互教学工具 Effect Institute 直观感受了这点。一个典型的 checkout 函数内部调用三个异步操作,任何一个都可能失败,而 try/catch 捕获到的错误永远是 unknown 类型——你只能靠此前「代码考古」得来的假设去做模式匹配,且类型检查器根本不会警告你的假设是否错误。文档里那句「如果新增了一个错误分支而你的 catch 没更新,恭喜你将在凌晨两点被叫醒」精准戳中了痛点。

在 Effect 里,用 Effect.fail 抛出一个 VeryBadRoll 错误后,若不处理,编译器会直接报类型错误,强迫你「与之和解」。新增第二种错误只需一个 TypeScript union,再用 catchTag 精确针对特定错误恢复处理——处理一个,错误类型就收窄一层。

TypeScript 对错误处理无能为力的根本原因在于:throw 语句在类型系统中是「透明的」,函数签名无法声明自己会抛出什么。相比之下,Java 有 checked exceptions,Rust 用 Result<T, E> 强制处理错误,Haskell 用 Either——这些语言都选择把错误路径纳入类型系统。Effect 的 Effect<Success, Error, Requirements> 本质上是同一思路在 TypeScript 生态的实现:它是一个描述计算的惰性值,而非立即执行的操作。这意味着整个程序可以被构造成一棵「效果树」,直到 Effect.runMain(或其他 runner)被调用时才真正执行。这种设计使得类型检查器在编译时就能追踪所有可能的失败路径,而不是等到运行时才通过 catch (e: unknown) 打捞残局。catchTag 能精确收窄错误类型,正是因为每个 TaggedError 都携带了字面量类型的 _tag 字段,供 TypeScript 的类型收窄机制识别。

从 succeed/fail 到并发与资源管理

Primeagen 逐步走过了 Effect 的基础构造器:Effect.succeed(总是成功)、Effect.fail(表示可恢复的错误)、Effect.sync(同步副作用,thunk 内不能抛错否则会变成 defect 缺陷)、Effect.try(可能失败的同步计算,比如经典的 JSON.parse)、以及对应的异步版本 Effect.promise / Effect.tryPromise 和回调版 Effect.callback

record 是个诡异的类型,literal keys 引发讨论

真正让他眼前一亮的是结构化并发(structured concurrency)。文档演示了并行发起多个 LLM 请求:在传统 Promise 世界里,你得手动接入 AbortController 做冗长的取消布线——他坦言「我对 AbortController 有噩梦」;而 Effect 中若一个请求失败,其他会被立即中断,这种能力是内建的。

他也注意到 Effect V4 把大量原先分散的包合并进了核心 effect 包,V4 还附带 17 个 unstable 模块(涵盖 HTTP、RPC、CLI、workflows、clustering 等),意在降低发现、安装、同步依赖的成本。

「结构化并发」(Structured Concurrency)是近年系统编程领域的重要概念,最早由 Nathaniel J. Smith 在 2018 年系统阐述,Python 的 asyncio Trio 库和 Kotlin 协程是其主要推广者。其核心主张是:并发任务的生命周期必须严格嵌套在创建它的作用域内——父任务结束前,所有子任务必须完成或被取消;任何子任务的失败都会向上传播并触发兄弟任务的取消。这与传统「即发即忘」(fire-and-forget)的 Promise 模型截然相反:Promise.all 本身不会在一个 Promise 失败时取消其他正在运行的 Promise,需要开发者手动接入 AbortController。Effect 将结构化并发作为运行时的内建保证,配合 Fiber(轻量级虚拟线程)实现,使得并发代码的取消、超时和资源释放语义变得可预期,从根本上消除了 Promise 世界里常见的「幽灵请求」和资源泄漏问题。

依赖注入:TJ 眼中最优雅的部分

直播后半段,开发者 TJ 连线加入,直接把话题拉到了 Effect 的「第三个维度」——Requirements(需求/上下文)。TJ 认为这是 Effect 最激动人心、也是他最看重的部分。

生成器让 yield 直接「执行 effect」,简化了大量样板代码

Random 服务为例:你像定义错误类型一样定义一个 Service(这里有个 TypeScript 惯用法,需要在两处引用自身才能工作,连 Effect 作者 kit langton 都承认「我们也希望不用这样」)。在 Effect.gen 里通过 yield 取出这个服务后,program 的类型第三个参数就会标注它依赖 Random——如果你不提供 Random,代码根本无法编译,而不是运行时出错

通过 Effect.provideServicepipe 组合多个服务,就能把依赖注入进去。TJ 强调了这在测试和 AI 协作中的杀伤力:你的 UserService 生产环境依赖 Postgres,但接口只是 getUser 返回 user 或 not-found error;测试时无需 mock 一堆假数据,直接写个返回 not-found 的空实现即可。更关键的是——你能从类型上确认某个服务不会私自打开 Postgres 连接,AI 就没法「在 for 循环里开关 85 次连接池」。

四年前 Effect 还没有生成器语法,需要写海量样板

TJ 同时点出了几个值得注意的实践建议:

  • 永远不要把裸 Error 写进错误类型,因为 Error 是所有错误的父类,会让类型「坍缩」成永远为真,彻底违背 Effect 的初衷。正确做法是用 Data.TaggedError 定义具名错误。
  • 用 Schema 取代 interface:他告诉 AI「写 interface 是非法的」,只能定义 schema,因为你需要真正 parse 数据、而非盲目相信 JSON.parse 出来的 any
  • Schema 与 HTTP API 结合最能体现威力——可以从规范推导出客户端,自动完成序列化/反序列化,字段类型不符时直接报错。

Effect 的依赖注入机制在思路上接近函数式编程中的 Reader Monad:把「需要什么环境才能运行」编码进类型,而非通过全局变量或隐式闭包传递依赖。传统 TypeScript 中,依赖注入通常借助 InversifyJS 等 IoC 容器,依赖关系在运行时解析,类型系统无从验证。Effect 的 Requirements 参数则让编译器成为依赖关系的守门人——若某个 Effect 的第三个类型参数非 never,它就不能被直接运行,必须先通过 provideServiceLayer 把依赖「填满」。Layer 是 Effect 中管理服务生命周期的高级抽象:它描述如何构建一个服务(包括资源获取与释放),并能组合成依赖图,Effect 运行时会自动拓扑排序、按需初始化,测试时只需将某个 Layer 替换为测试实现,而无需改动任何业务逻辑代码。

分歧与争议:抽象是否值得

这场直播也如实呈现了社区的分歧。有观众尖锐评论:这看起来更像「用误导性工程去掩盖糟糕工程」,runSync 不过是方法的惰性执行,是在「重新发明 JS」,把 CPU 周期交给机器管理。也有人更偏爱 Go 的显式错误处理方式。

Primeagen 对 Go 的做法表示认同——错误来源清晰直白,但也指出 Go 在 JavaScript 那种「把一堆 Promise 复用聚合」的场景下没那么简单。他对某些语法直言不讳地吐槽「cursed syntax」「evil」,尤其是生成器的 yieldyield* 语义(一个是「送出」,一个是「拉入」),以及需要靠 arity(参数个数)来决定调用哪个函数的设计。

不过在最没争议的一点上,双方达成共识:知道会发生哪些错误是有用的。TJ 甚至觉得好笑——「这是 Effect 里最不该被辩论的部分」。至于「写出所有错误很啰嗦」这样的批评,他认为是公平的权衡取舍。

直播因 Primeagen 的喉咙旧伤(两年多前的奇怪咽喉损伤,连续说话超过两小时就会疼)而提前结束。他坦言这场更像是基础展示(barely a showcase),真正体现 Effect 价值的部分——搭一个服务器、做一个 to-do 应用、接入 Solid.js 前端——留到了下一场。TJ 也建议:与其死磕文档,不如让 AI 生成一个最小化的 bun HTTP 服务器,从 /health 端点起步,在动手中体会类型推断的爽感。

结语

这场看似散漫的直播,浓缩了 AI 时代一个真实的学习图景:AI 能生成代码骨架,但**「能看懂」才是验收线**;工具越强,理解原理越是护城河而非负担。Effect 的三层价值——类型化错误、结构化并发、优雅的依赖注入——每一项单看或许都有替代品,但组合起来构成了一套让 AI 难以「作恶」、让人类得以放心测试边界的编程范式。正如 Primeagen 所说,学习本身在今天竟需要被「捍卫」,而这恰恰是它最不该被质疑的时候。

分享:

相关推荐