关于 effect-ts
effect-ts 是一项为使用 Effect 库开发提供指导的 skill,面向 Effect 项目开发者。内容涵盖类型化错误处理、依赖注入、组合、资源管理、并发及测试模式。
主要功能
- 01类型化错误处理模式
- 02用于依赖注入的 Services 和 Layers
- 03可组合的 Effect 与资源作用域
- 04并发、流和调度指导
- 05测试及代码模式研究
适用场景
- 01设计 Effect 程序的错误处理
- 02为应用添加服务与层
- 03管理异步任务和资源
- 04应用 Effect 测试模式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sablier-labs/ agent-skills --skill effect-ts安装后在 Claude Code 中输入/effect-ts确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/critical-rules.mdeffect-atom.mdnext-js.mdoption-null.mdstreams.mdtesting.md详细介绍
Effect-TS 专家
提供使用 Effect 库进行函数式编程的专家指导,涵盖错误处理、依赖注入、可组合性和测试模式。
前置条件检查
开始任何与 Effect 相关的工作之前,请确认 Effect-TS 源代码存在于 ~/.effect。
如果不存在,请立即停止并告知用户。 继续之前先克隆它:
git clone https://github.com/Effect-TS/effect.git ~/.effect
研究策略
Effect-TS 有多种方式可以完成同一项任务。使用 Effect 模式时,请主动通过 Task 工具生成研究 Agent,调查最佳实践,尤其是在处理中等到高复杂度任务时。
研究来源(按优先级排序)
-
优先查看代码库模式 — 实现前,先检查当前项目中类似的模式。如果代码库中存在 Effect 模式,请遵循这些模式以保持一致。如果不存在,则跳过此步骤。
-
Effect 源代码 — 遇到复杂的类型错误、行为不明确或实现细节问题时,请查看
~/.effect/packages/effect/src/中的 Effect 源代码。这里包含 Effect 核心逻辑和模块。
何时进行研究
高优先级(始终研究):
- 实现 Service、Layer 或复杂的依赖注入
- 处理包含多种错误类型或复杂错误层次结构的错误
- 基于 Stream 的操作和响应式模式
- 使用作用域 Effect 进行资源管理和清理
- 并发/并行操作和性能关键型代码
- 测试模式,尤其是不熟悉的测试场景
中优先级(复杂时研究):
- 将命令式代码(try-catch、Promise)重构为 Effect 模式
- 添加新的 Service 依赖或重构 Service 层
- 自定义错误类型或扩展现有错误层次结构
- 与外部系统(数据库、API、第三方服务)集成
研究方法
- 调查多个相关模式时,同时生成多个 Task Agent
- 优先寻找规范、易读且易维护的解决方案,而非巧妙的优化
- 若存在相关模式,请依据现有代码库模式验证建议方案的一致性
- 存在多种可行方案时,请研究并找出最符合 Effect-TS 惯用方式的解决方案
代码库模式发现
在使用 Effect 的项目中工作时,实现新代码前请检查现有模式:
- 搜索 Effect 导入 — 查找从
'effect'导入的文件,以了解现有用法 - 识别 Service 模式 — 找出项目中 Service 和 Layer 的组织方式
- 留意错误处理约定 — 检查错误的定义和传播方式
- 检查测试模式 — 查看项目中 Effect 代码的测试方式
如果代码库中不存在 Effect 模式,请采用 Effect 源代码和示例中的规范模式继续工作。不要因缺少代码库模式而停滞。
Effect 原则
编写 Effect 代码时,请遵循以下核心原则:
错误处理
- 使用 Effect 的类型化错误系统,而非抛出异常
- 定义描述明确的错误类型,并妥善传播错误
- 使用
Effect.fail、Effect.catchTag、Effect.catchAll控制错误流程 - 禁止使用的模式请参阅
./references/critical-rules.md
依赖注入
- 使用 Service 和 Layer 实现依赖注入
- 使用
Context.Tag定义 Service - 使用
Layer.merge、Layer.provide组合 Layer - 使用
Effect.provide注入依赖
可组合性
- 利用 Effect 的可组合性处理复杂操作
- 使用适当的构造器:
Effect.succeed、Effect.fail、Effect.tryPromise、Effect.try - 使用作用域 Effect 进行适当的资源管理
- 使用
Effect.flatMap、Effect.map、Effect.tap串联操作
代码质量
- 编写类型安全的代码,充分利用 Effect 的类型系统
- 使用
Effect.gen编写清晰易读的顺序代码 - 使用 Effect 的测试工具实现恰当的测试模式
- 优先使用
Effect.fn(),以自动收集遥测数据并获得更好的堆栈跟踪
关键规则
编写任何 Effect 代码之前,请阅读并牢记 ./references/critical-rules.md。关键指南:
- 无效做法: 在 Effect.gen 中使用 try-catch(Effect 失败不会被抛出)
- 避免: 类型断言(as never/any/unknown)
- 推荐: 使用
return yield*模式处理错误(明确表示执行终止)
常见失败模式
快速查看经常导致问题的模式:
- SubscriptionRef 版本不匹配 —
unsafeMake is not a function→ 快速参考 - 取消与失败 — 中断不是错误 → 错误分类
- Option 与 null — 内部使用 Option,边界处使用 null → option-null.md
- Stream 背压 — 无限流会挂起 → streams.md
解释解决方案
提供解决方案时,请解释所用的 Effect-TS 概念,以及这些概念为何适用于特定用例。如果遇到文档未涵盖的模式,请在保持与现有代码库模式一致(如有)的前提下,提出改进建议。
快速参考
创建 Effect
Effect.succeed(value) // Wrap success value
Effect.fail(error) // Create failed effect
Effect.tryPromise(fn) // Wrap promise-returning function
Effect.try(fn) // Wrap synchronous throwing function
Effect.sync(fn) // Wrap synchronous non-throwing function
组合 Effect
Effect.flatMap(effect, fn) // Chain effects
Effect.map(effect, fn) // Transform success value
Effect.tap(effect, fn) // Side effect without changing value
Effect.all([...effects]) // Run effects (concurrency configurable)
Effect.forEach(items, fn) // Map over items with effects
// Collect ALL errors (not just first)
Effect.all([e1, e2, e3], { mode: "validate" }) // Returns all failures
// Partial success handling
Effect.partition([e1, e2, e3]) // Returns [failures, successes]
错误处理
// Define typed errors with Data.TaggedError (preferred)
class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
userId: string
}> {}
// Direct yield of errors (no Effect.fail wrapper needed)
Effect.gen(function* () {
if (!user) {
return yield* new UserNotFoundError({ userId })
}
})
Effect.catchTag(effect, tag, fn) // Handle specific error tag
Effect.catchAll(effect, fn) // Handle all errors
Effect.result(effect) // Convert to Exit value
Effect.orElse(effect, alt) // Fallback effect
错误分类
按类别划分错误,以便妥善处理:
| 类别 | 示例 | 处理方式 |
|---|---|---|
| 预期拒绝 | 用户取消、拒绝 | 平稳退出,不重试 |
| 领域错误 | 验证、业务规则 | 向用户显示,不重试 |
| 缺陷 | Bug、断言 | 记录日志并告警,进行调查 |
| 中断 | Fiber 取消、超时 | 清理,必要时可重试 |
| 未知/外部错误 | 抛出的异常 | 在边界处规范化 |
// Pattern: Normalize unknown errors at boundary
const safeBoundary = Effect.catchAllDefect(effect, (defect) =>
Effect.fail(new UnknownError({ cause: defect }))
)
// Pattern: Catch user-initiated cancellations separately
Effect.catchTag(effect, "UserCancelledError", () => Effect.succeed(null))
// Pattern: Handle interruptions differently from failures
Effect.onInterrupt(effect, () => Effect.log("Operation cancelled"))
模式匹配(Match 模块)
用于带标签联合类型和复杂条件的默认分支工具。
import { Match } from "effect"
// Type-safe exhaustive matching on tagged errors
const handleError = Match.type<AppError>().pipe(
Match.tag("UserCancelledError", () => null), // Expected rejection
Match.tag("ValidationError", (e) => e.message), // Domain error
Match.tag("NetworkError", () => "Connection failed"), // Retryable
Match.exhaustive // Compile error if case missing
)
// Replace nested catchTag chains
// BEFORE: effect.pipe(catchTag("A", ...), catchTag("B", ...), catchTag("C", ...))
// AFTER:
Effect.catchAll(effect, (error) =>
Match.value(error).pipe(
Match.tag("A", handleA),
Match.tag("B", handleB),
Match.tag("C", handleC),
Match.exhaustive
)
)
// Match on values (cleaner than if/else)
const describe = Match.value(status).pipe(
Match.when("pending", () => "Loading..."),
Match.when("success", () => "Done!"),
Match.orElse(() => "Unknown")
)
服务与 Layer
// Pattern 1: Context.Tag (implementation provided separately via Layer)
class MyService extends Context.Tag("MyService")<MyService, { ... }>() {}
const MyServiceLive = Layer.succeed(MyService, { ... })
Effect.provide(effect, MyServiceLive)
// Pattern 2: Effect.Service (default implementation bundled)
class UserRepo extends Effect.Service<UserRepo>()("UserRepo", {
effect: Effect.gen(function* () {
const db = yield* Database
return { findAll: db.query("SELECT * FROM users") }
}),
dependencies: [Database.Default], // Optional service dependencies
accessors: true // Auto-generate method accessors
}) {}
Effect.provide(effect, UserRepo.Default) // .Default layer auto-generated
// Use UserRepo.DefaultWithoutDependencies when deps provided separately
// Effect.Service with parameters (3.16.0+)
class ConfiguredApi extends Effect.Service<ConfiguredApi>()("ConfiguredApi", {
effect: (config: { baseUrl: string }) =>
Effect.succeed({ fetch: (path: string) => `${config.baseUrl}/${path}` })
}) {}
// Pattern 3: Context.Reference (defaultable tags - 3.11.0+)
class SpecialNumber extends Context.Reference<SpecialNumber>()(
"SpecialNumber",
{ defaultValue: () => 2048 }
) {}
// No Layer required if default value suffices
// Pattern 4: Context.ReadonlyTag (covariant - 3.18.0+)
// Use for functions that consume services without modifying the type
function effectHandler<I, A, E, R>(service: Context.ReadonlyTag<I, Effect.Effect<A, E, R>>) {
// Handler can use service in a covariant position
}
生成器模式
Effect.gen(function* () {
const a = yield* effectA;
const b = yield* effectB;
if (error) {
return yield* Effect.fail(new MyError());
}
return result;
});
// Effect.fn - automatic tracing and telemetry (preferred for named functions)
const fetchUser = Effect.fn("fetchUser")(function* (id: string) {
const db = yield* Database
return yield* db.query(id)
})
// Creates spans, captures call sites, provides better stack traces
资源管理
Effect.acquireUseRelease(acquire, use, release) // Bracket pattern
Effect.scoped(effect) // Scope lifetime to effect
Effect.addFinalizer(cleanup) // Register cleanup action
Duration
Effect 在任何预期输入为 DurationInput 的位置都接受易读的时长字符串:
// String syntax (preferred) - singular or plural forms work
Duration.toMillis("5 minutes") // 300000
Duration.toMillis("1 minute") // 60000
Duration.toMillis("30 seconds") // 30000
Duration.toMillis("100 millis") // 100
// Verbose syntax (avoid)
Duration.toMillis(Duration.minutes(5)) // Same result, more verbose
// Common units: millis, seconds, minutes, hours, days, weeks
// Also: nanos, micros
调度
Effect.retry(effect, Schedule.exponential("100 millis")) // Retry with backoff
Effect.repeat(effect, Schedule.fixed("1 second")) // Repeat on schedule
Schedule.compose(s1, s2) // Combine schedules
状态管理
Ref.make(initialValue) // Mutable reference
Ref.get(ref) // Read value
Ref.set(ref, value) // Write value
Deferred.make<E, A>() // One-time async value
SubscriptionRef(响应式引用)
// WARNING: Never use unsafeMake - it may not exist in your Effect version.
// If you see "unsafeMake is not a function", use the safe API below.
SubscriptionRef.make(initial) // Create reactive reference (safe)
SubscriptionRef.get(ref) // Read current value
SubscriptionRef.set(ref, value) // Update value (notifies subscribers)
SubscriptionRef.changes(ref) // Stream of value changes
// React integration (effect-atom pattern)
const ref = yield* SubscriptionRef.make<User | null>(null)
// Hook reads: useSubscriptionRef(ref) — returns current value or null
// Handle null explicitly in components
并发
Effect.fork(effect) // Run in background fiber
Fiber.join(fiber) // Wait for fiber result
Effect.race(effect1, effect2) // First to complete wins
Effect.all([...effects], { concurrency: "unbounded" })
配置与环境变量
import { Config, ConfigProvider, Effect, Layer, Redacted } from "effect"
// Basic config values
const port = Config.number("PORT") // Required number
const host = Config.string("HOST").pipe( // Optional with default
Config.withDefault("localhost")
)
// Sensitive values (masked in logs)
const apiKey = Config.redacted("API_KEY") // Returns Redacted<string>
const secret = Redacted.value(yield* apiKey) // Unwrap when needed
// Nested configuration with prefix
const dbConfig = Config.all({
host: Config.string("HOST"),
port: Config.number("PORT"),
name: Config.string("NAME"),
}).pipe(Config.nested("DATABASE")) // DATABASE_HOST, DATABASE_PORT, etc.
// Using config in effects
const program = Effect.gen(function* () {
const p = yield* Config.number("PORT")
const key = yield* Config.redacted("API_KEY")
return { port: p, apiKey: Redacted.value(key) }
})
// Custom config provider (e.g., from object instead of env)
const customProvider = ConfigProvider.fromMap(
new Map([["PORT", "3000"], ["API_KEY", "secret"]])
)
const withCustomConfig = Effect.provide(
program,
Layer.setConfigProvider(customProvider)
)
// Config validation and transformation
const validPort = Config.number("PORT").pipe(
Config.validate({
message: "Port must be between 1 and 65535",
validation: (n) => n >= 1 && n <= 65535,
})
)
数组操作
import { Array as Arr, Order } from "effect"
// Sorting with built-in orderings (accepts any Iterable)
Arr.sort([3, 1, 2], Order.number) // [1, 2, 3]
Arr.sort(["b", "a", "c"], Order.string) // ["a", "b", "c"]
Arr.sort(new Set([3n, 1n, 2n]), Order.bigint) // [1n, 2n, 3n]
// Sort by derived value
Arr.sortWith(users, (u) => u.age, Order.number)
// Sort by multiple criteria
Arr.sortBy(
users,
Order.mapInput(Order.number, (u: User) => u.age),
Order.mapInput(Order.string, (u: User) => u.name)
)
// Built-in orderings: Order.string, Order.number, Order.bigint, Order.boolean, Order.Date
// Reverse ordering: Order.reverse(Order.number)
工具函数
import { constVoid as noop } from "effect/Function"
// constVoid returns undefined, useful as a no-operation callback
noop() // undefined
// Common use cases:
Effect.tap(effect, noop) // Ignore value, just run effect
Promise.catch(noop) // Swallow errors
eventEmitter.on("event", noop) // Register empty handler
弃用项
BigDecimal.fromNumber— 改用BigDecimal.unsafeFromNumber(3.11.0+)Schema.annotations()— 现在会移除先前设置的标识符注解;标识符仅与 schema 的ast引用绑定(3.17.10)
补充资源
本地 Effect 资源
~/.effect/packages/effect/src/— Effect 核心模块及其实现
外部资源
参考文件
./references/critical-rules.md— 禁止的模式和强制约定./references/effect-atom.md— 用于 React 的 Effect-Atom 响应式状态管理./references/next-js.md— Effect 与 Next.js 15+ App Router 集成模式./references/option-null.md— Option 与 null 的边界模式./references/streams.md— Stream 模式和背压注意事项./references/testing.md— Vitest 确定性测试模式