☰
初学 Effect 必翻的 5 个车:strict 模式、依赖注入和那个“必装“的 IDE 插件
2026/10/20 2:35:54 网站建设 项目流程

初学 Effect 必翻的 5 个车:strict 模式、依赖注入和那个"必装"的 IDE 插件

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

Effect 是当下 TypeScript 函数式生态里增长最猛的项目之一:它把同步/异步、成功/失败、依赖注入统一建模成一个Effect.Effect类型,把"隐式业务风险"显式编码进类型系统,让opencode、Sandcastle一类大型项目敢用它替代 NestJS 落地生产。仓库当前的主版本是 4.x LTS(packages/effect/package.json 中版本号为 4.0.2),官方对它的定位是"production-grade applications"。

但越是把"风险前置到编译期"的框架,入门时的翻车点就越反直觉。下面这 5 个坑,几乎每个初学 Effect 的人都会踩到至少两个:从 tsconfig 的 strict 开关、模块解析的 exports 条件,到那个藏在编译器插件里的"IDE 外挂",再到 A/E/R 三元组与 Layer 的组合时序。

坑 1:strict 模式没开,类型契约先崩一半

Effect 的官方 README 把严格类型检查列进了硬性要求,原文非常直白:

Strict type-checking:thestrictflag must be enabled in yourtsconfig.json.

这不是建议,是前提(见 README.md)。Effect 的类型系统高度依赖strict下可靠的discriminated union收窄、exactOptionalPropertyTypes这类增强选项,才能把"错误类型"和"依赖类型"精确地推导出来。如果你沿用某个老项目的strict: false配置,最常见的连锁反应是:Effect.catchTag("ParseError", ...)里的分支收窄失效,错误联合类型退化成unknown,类型契约直接形同虚设。

仓库自己的基础配置可以作为范本(tsconfig.base.json),除了strict,还开了exactOptionalPropertyTypes、noUnusedLocals、noImplicitOverride、noErrorTruncation等一系列"越严格越好"的选项。初学者至少应该先保证:

{ "compilerOptions": { "strict": true, "exactOptionalPropertyTypes": true, "noErrorTruncation": true } }

noErrorTruncation尤其容易被忽略——Effect 的错误类型动辄是几十行的联合类型,默认截断会让你根本看不清"缺了哪个分支"。

坑 2:模块解析的 exports 条件没配对,连 import 都过不去

社区里关于 opencode 这类大型 Effect 项目的配置实践,几乎都提到了一个共同雷区:exports字段的条件(conditions)没配好,模块解析直接崩。看 Effect 自己的包结构就明白了(packages/effect/package.json):

"exports": { ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, "./*/index": null }

仓库内开发时,effect/schema、effect/cli这些子路径全部直接指向src下的 TypeScript 源码;而发布时通过publishConfig.exports再映射到dist/*.js。同时注意那两行null——internal内部路径被显式封死,任何试图import ... from "effect/internal/xxx"的代码在解析阶段就会被拒绝。

翻车的典型场景有两类:

  • 条件顺序错:在 bundler(如 Vite)或 Node 环境下,exports条件按声明顺序匹配。社区文章提到的"browser 条件"就是一个例子——如果browser条件指向的文件缺失,或default条件写在了node前面,解析器会选中错误的入口,轻则 API 对不上,重则直接抛ERR_PACKAGE_PATH_NOT_EXPORTED。
  • 依赖了被封死的内部路径:很多教程片段会直接 importeffect/internal/*,换到正式安装的包版本后当场 404。

初学者的安全姿势是:只 import 官方文档和类型声明里出现过的公开子路径(effect、effect/schema、effect/cli等),并且在 tsconfig 里保持moduleResolution与运行环境一致,别让 bundler 的条件解析和 tsc 各自为政。

坑 3:没装 Effect 语言服务插件,复杂类型推导直接躺平

这是社区文章反复强调"必装"的一项:Effect 语言服务插件(@effect/language-service)。它不是独立的 VSCode 扩展,而是作为 TypeScript / tsgo 的 language plugin 挂进 tsconfig(tsconfig.base.json):

"plugins": [{ "name": "@effect/language-service", "namespaceImportPackages": ["effect", "@effect/*"], "diagnosticSeverity": { "globalErrorInEffectFailure": "off", "unstableApiUsage": "off" }, "overrides": [ { "exclude": ["**/internal/**/*", "**/index.ts"], "options": { "diagnosticSeverity": { "apiStabilityLeak": "error" } } } ] }]

不装它,最直接的损失是三类诊断全部缺失:

  • floatingEffect:写了一个Effect但既没run也没pipe处理,相当于"丢弃了未执行的副作用"——插件会提醒你这是个未消费的 Effect(ai-docs/tsconfig.json 中有该诊断的开关示例);
  • preferSchemaOverJson:该用Schema做运行时校验的地方用了裸 JSON,插件会提示更优写法;
  • apiStabilityLeak:在库的公开 API 里泄漏了内部类型或 unstable API——仓库里专门用overrides把这些场景升成error。

Effect 的复杂类型(错误联合、依赖需求、Effect.fn的返回类型)依赖 TS 的精确推导,配合插件才能获得"代码即契约"的编辑体验。官方 README 也明确推荐 TypeScript 5.9 及以上版本,并建议用 TypeScript 7 以获取与其 TypeScript tooling(tsgo)的最佳兼容性(README.md)。

坑 4:路径别名配错,同一份类型出现两个"平行宇宙"

Effect 的 monorepo 本身大量使用paths别名——测试代码里把effect解析回源码,既加速类型检查,也避开包之间的循环依赖(tsconfig.tests.json):

"paths": { "effect": ["./packages/effect/src/index.ts"], "effect/*": ["./packages/effect/src/*.ts"], "@effect/ai-anthropic": ["./packages/ai/anthropic/src/index.ts"] }

这是仓库内部的工程策略,但把同样的思路搬到用户项目时容易翻车:同一个包被解析成两份不同的拷贝。比如 alias 指向node_modules/effect的旧版本,而另一个依赖链解析到新版本,TypeScript 会认为它们是两个不相容的类型——Context.Service的服务标识、"错误类型"匹配全部失效,报错却让人完全摸不着头脑("类型 A 不能赋值给类型 A")。

教训有两条:alias 必须指向且只指向一份源码;引入新包后先确认npm ls/ pnpm 的解析树里 effect 只有一个版本。Effect 的类型系统是"全链路参与"的,任何一个环节解析出副本,契约就断。

坑 5:A/E/R 三元组误用,把 Effect 写成 Promise 的平替

社区对 Effect 从 fp-ts 演进的分析有一个共识:Effect 的核心贡献是把隐式风险显式编码为类型。反映到 API 上就是Effect.Effect<A, E, R>三元组——成功类型 A、错误类型 E、依赖类型 R。看仓库的错误处理示例(ai-docs/src/01_effect/04_errors/01_error-handling.ts):

declare const loadPort: (input: string) => Effect.Effect<number, ParseError | ReservedPortError> export const recovered = loadPort("80").pipe( Effect.catchTag(["ParseError", "ReservedPortError"], (_) => Effect.succeed(3000)) )

初学者的三个典型误用:

  1. 把 E 写成unknown或any:等于亲手销毁了 typed error 的全部价值,catchTag的精确匹配就无从谈起;
  2. 在 Effect 生成器里用throw而不是return yield* error:仓库的Effect.fn文档和示例反复强调"Always return when raising an error"——throw会让 TS 认为函数还会继续执行,控制流分析直接错乱(ai-docs/src/01_effect/01_basics/02_effect-fn.ts);
  3. 把 R 位漏掉:很多人把 Effect 当"带类型错误的 Promise"用,忘了第三位参数记录的是依赖需求,这恰恰是把 DI 信息写进类型的核心。

顺带一提写法规范:仓库的写作指引明确"内联代码优先Effect.gen,可复用函数优先Effect.fn("name"),热路径用Effect.fnUntraced,避免写一个只包装Effect.gen的空壳函数"(ai-docs/src/01_effect/01_basics/index.md)。

坑 6:依赖注入的时序——编译期"需求清单"没喂饱

Effect 的 DI 是编译期可验证的:服务的"需求清单"直接写在类型里。定义一个服务用Context.Service,实现用Layer.effect(ai-docs/src/01_effect/03_services/01_service.ts):

export class Database extends Context.Service<Database, { query(sql: string): Effect.Effect<Array<unknown>, DatabaseError> }>()("myapp/db/Database") { static readonly layer = Layer.effect( Database, Effect.gen(function*() { const query = Effect.fn("Database.query")(function*(sql: string) { yield* Effect.log("Executing SQL query:", sql) return [{ id: 1, name: "Alice" }] }) return Database.of({ query }) }) ) }

初学者在这里翻车的两个点:

  • 需求没喂饱就 run:服务实现里yield* SqlClient.SqlClient,R 位立刻变成SqlClient;如果最后Effect.run*之前没有用Layer.provide补上依赖,类型检查会直接告诉你"requirements 未被满足"。这正是编译期 DI 与反射式容器最大的区别——漏依赖不可能等到运行时才爆;
  • Layer.provide与Layer.provideMerge语义混淆:仓库示例讲得很清楚——provide只对外暴露当前服务的接口,provideMerge会把被依赖的服务也一并暴露给下游(ai-docs/src/01_effect/03_services/20_layer-composition.ts):
static readonly layer: Layer.Layer<UserRepository, Config.ConfigError | SqlError.SqlError> = this.layerNoDeps.pipe(Layer.provide(SqlClientLayer)) static readonly layerWithSqlClient: Layer.Layer<UserRepository | SqlClient.SqlClient, ...> = this.layerNoDeps.pipe(Layer.provideMerge(SqlClientLayer))

还有一个隐蔽的时序问题:服务标识字符串(如"myapp/db/Database")必须全局唯一。同一路径下的同名服务一旦标识冲突,Layer 组合时就会出现"这个实现喂给了另一个服务"的诡异行为,排查起来极其痛苦。

少踩坑的检查清单

把这些坑收敛成一张入门 checklist:

  1. strict必须开,最好连exactOptionalPropertyTypes、noErrorTruncation一起开;
  2. 只 import 公开子路径,别碰effect/internal/*,并确认 bundler 的 exports 条件与运行环境一致;
  3. tsconfigplugins里挂上@effect/language-service,让 floatingEffect、preferSchemaOverJson、apiStabilityLeak 这类诊断替你兜底;
  4. 路径别名指向唯一的一份源码,别让同一包出现多个副本;
  5. 把Effect.Effect<A, E, R>当"带契约的流程描述"而不是"Promise 平替",错误用return yield* error而不是throw;
  6. 理解provide与provideMerge的暴露差异,保证 run 之前需求清单被喂饱。

Effect 的陡峭学习曲线,本质上是把"别人在运行时才炸的问题"提前到编译期和编辑器里。这几个坑绕过去之后,你会发现它给的回报是实打实的:错误处理、依赖注入、并发与可观测性,全部收敛到一套类型系统里,让大型 TypeScript 项目第一次有了"改一处、编译器帮你看全局"的底气。

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询