名称: effect-best-practices 描述: 强制执行Effect-TS的模式,用于服务、错误、层和原子。在使用Effect.Service、Schema.TaggedError、Layer组合或effect-atom React组件编写代码时使用。 元数据: 版本: 1.0.0
Effect-TS 最佳实践
这个技能强制执行Effect-TS代码库的固执己见、一致的模式。这些模式优化类型安全、可测试性、可观察性和可维护性。
快速参考: 关键规则
| 类别 | 做 | 不做 |
|---|---|---|
| 服务 | Effect.Service 带 accessors: true |
Context.Tag 用于业务逻辑 |
| 依赖 | dependencies: [Dep.Default] 在服务中 |
手动 Layer.provide 在使用站点 |
| 错误 | Schema.TaggedError 带 message 字段 |
普通类或通用Error |
| 错误特异性 | UserNotFoundError, SessionExpiredError |
通用 NotFoundError, BadRequestError |
| 错误处理 | catchTag/catchTags |
catchAll 或 mapError |
| ID | Schema.UUID.pipe(Schema.brand("@App/EntityId")) |
普通 string 用于实体ID |
| 函数 | Effect.fn("Service.method") |
匿名生成器 |
| 日志 | Effect.log 带结构化数据 |
console.log |
| 配置 | Config.* 带验证 |
直接使用 process.env |
| 选项 | Option.match 带两种情况 |
Option.getOrThrow |
| 可空性 | Option<T> 在域类型中 |
null/undefined |
| 原子 | Atom.make 在组件外 |
在渲染中创建原子 |
| 原子状态 | Atom.keepAlive 用于全局状态 |
忘记keepAlive用于持久状态 |
| 原子更新 | useAtomSet 在React组件中 |
从React命令式 Atom.update |
| 原子清理 | get.addFinalizer() 用于副作用 |
缺少事件监听器的清理 |
| 原子结果 | Result.builder 带 onErrorTag |
忽略加载/错误状态 |
服务定义模式
始终使用 Effect.Service 用于业务逻辑服务。这提供了自动访问器、内置 Default 层和正确的依赖声明。
import { Effect } from "effect"
export class UserService extends Effect.Service<UserService>()("UserService", {
accessors: true,
dependencies: [UserRepo.Default, CacheService.Default],
effect: Effect.gen(function* () {
const repo = yield* UserRepo
const cache = yield* CacheService
const findById = Effect.fn("UserService.findById")(function* (id: UserId) {
const cached = yield* cache.get(id)
if (Option.isSome(cached)) return cached.value
const user = yield* repo.findById(id)
yield* cache.set(id, user)
return user
})
const create = Effect.fn("UserService.create")(function* (data: CreateUserInput) {
const user = yield* repo.create(data)
yield* Effect.log("User created", { userId: user.id })
return user
})
return { findById, create }
}),
}) {}
// 用法 - 依赖已经连接
const program = Effect.gen(function* () {
const user = yield* UserService.findById(userId)
return user
})
// 在应用根
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default)
当 Context.Tag 可接受时:
- 运行时注入的基础设施(Cloudflare KV, worker绑定)
- 资源外部提供的工厂模式
参见 references/service-patterns.md 获取详细模式。
错误定义模式
始终使用 Schema.TaggedError 用于错误。这使它们可序列化(RPC所需)并提供一致的结构。
import { Schema } from "effect"
import { HttpApiSchema } from "@effect/platform"
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
"UserNotFoundError",
{
userId: UserId,
message: Schema.String,
},
HttpApiSchema.annotations({ status: 404 }),
) {}
export class UserCreateError extends Schema.TaggedError<UserCreateError>()(
"UserCreateError",
{
message: Schema.String,
cause: Schema.optional(Schema.String),
},
HttpApiSchema.annotations({ status: 400 }),
) {}
错误处理 - 使用 catchTag/catchTags:
// 正确 - 保留类型信息
yield* repo.findById(id).pipe(
Effect.catchTag("DatabaseError", (err) =>
Effect.fail(new UserNotFoundError({ userId: id, message: "Lookup failed" }))
),
Effect.catchTag("ConnectionError", (err) =>
Effect.fail(new ServiceUnavailableError({ message: "Database unreachable" }))
),
)
// 正确 - 一次多个标签
yield* effect.pipe(
Effect.catchTags({
DatabaseError: (err) => Effect.fail(new UserNotFoundError({ userId: id, message: err.message })),
ValidationError: (err) => Effect.fail(new InvalidEmailError({ email: input.email, message: err.message })),
}),
)
偏好显式而非通用错误
每个不同的失败原因都应拥有自己的错误类型。 不要将多个失败模式合并为通用HTTP错误。
// 错误 - 通用错误丢失信息
export class NotFoundError extends Schema.TaggedError<NotFoundError>()(
"NotFoundError",
{ message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
// 然后映射所有到它:
Effect.catchTags({
UserNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })),
ChannelNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })),
MessageNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })),
})
// 前端得到无用信息: { _tag: "NotFoundError", message: "Not found" }
// 哪个资源?用户?频道?消息?无法判断!
// 正确 - 具有丰富上下文的显式域错误
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
"UserNotFoundError",
{ userId: UserId, message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
export class ChannelNotFoundError extends Schema.TaggedError<ChannelNotFoundError>()(
"ChannelNotFoundError",
{ channelId: ChannelId, message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
export class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()(
"SessionExpiredError",
{ sessionId: SessionId, expiredAt: Schema.DateTimeUtc, message: Schema.String },
HttpApiSchema.annotations({ status: 401 }),
) {}
// 前端现在可以显示特定UI:
// - UserNotFoundError → "用户不存在"
// - ChannelNotFoundError → "频道已被删除"
// - SessionExpiredError → "您的会话已过期。请重新登录。"
参见 references/error-patterns.md 获取错误重映射和重试模式。
模式与品牌类型模式
品牌所有实体ID 以在服务边界上保证类型安全:
import { Schema } from "effect"
// 实体ID - 始终品牌化
export const UserId = Schema.UUID.pipe(Schema.brand("@App/UserId"))
export type UserId = Schema.Schema.Type<typeof UserId>
export const OrganizationId = Schema.UUID.pipe(Schema.brand("@App/OrganizationId"))
export type OrganizationId = Schema.Schema.Type<typeof OrganizationId>
// 域类型 - 使用 Schema.Struct
export const User = Schema.Struct({
id: UserId,
email: Schema.String,
name: Schema.String,
organizationId: OrganizationId,
createdAt: Schema.DateTimeUtc,
})
export type User = Schema.Schema.Type<typeof User>
// 输入类型用于突变
export const CreateUserInput = Schema.Struct({
email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)),
name: Schema.String.pipe(Schema.minLength(1)),
organizationId: OrganizationId,
})
export type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>
何时不品牌化:
- 不跨越服务边界的简单字符串(URLs, 文件路径)
- 原始配置值
参见 references/schema-patterns.md 获取转换和高级模式。
函数模式与 Effect.fn
始终使用 Effect.fn 用于服务方法。这提供具有适当跨度名称的自动跟踪:
// 正确 - Effect.fn 带描述性名称
const findById = Effect.fn("UserService.findById")(function* (id: UserId) {
yield* Effect.annotateCurrentSpan("userId", id)
const user = yield* repo.findById(id)
return user
})
// 正确 - Effect.fn 带多个参数
const transfer = Effect.fn("AccountService.transfer")(
function* (fromId: AccountId, toId: AccountId, amount: number) {
yield* Effect.annotateCurrentSpan("fromId", fromId)
yield* Effect.annotateCurrentSpan("toId", toId)
yield* Effect.annotateCurrentSpan("amount", amount)
// ...
}
)
层组合
在服务中声明依赖,而不是在使用站点:
// 正确 - 依赖在服务定义中
export class OrderService extends Effect.Service<OrderService>()("OrderService", {
accessors: true,
dependencies: [
UserService.Default,
ProductService.Default,
PaymentService.Default,
],
effect: Effect.gen(function* () {
const users = yield* UserService
const products = yield* ProductService
const payments = yield* PaymentService
// ...
}),
}) {}
// 在应用根 - 简单合并
const AppLive = Layer.mergeAll(
OrderService.Default,
// 基础设施层(故意不在依赖中)
DatabaseLive,
RedisLive,
)
参见 references/layer-patterns.md 获取测试层和配置依赖层。
选项处理
永远不要使用 Option.getOrThrow。始终显式处理两种情况:
// 正确 - 显式处理
yield* Option.match(maybeUser, {
onNone: () => Effect.fail(new UserNotFoundError({ userId, message: "Not found" })),
onSome: (user) => Effect.succeed(user),
})
// 正确 - 使用 getOrElse 用于默认值
const name = Option.getOrElse(maybeName, () => "Anonymous")
// 正确 - Option.map 用于转换
const upperName = Option.map(maybeName, (n) => n.toUpperCase())
Effect Atom(前端状态)
Effect Atom 提供与Effect集成的React响应式状态管理。
基本原子
import { Atom } from "@effect-atom/atom-react"
// 在组件外定义原子
const countAtom = Atom.make(0)
// 使用 keepAlive 用于应持久的全局状态
const userPrefsAtom = Atom.make({ theme: "dark" }).pipe(Atom.keepAlive)
// 原子家族用于每实体状态
const modalAtomFamily = Atom.family((type: string) =>
Atom.make({ isOpen: false }).pipe(Atom.keepAlive)
)
React 集成
import { useAtomValue, useAtomSet, useAtom, useAtomMount } from "@effect-atom/atom-react"
function Counter() {
const count = useAtomValue(countAtom) // 只读
const setCount = useAtomSet(countAtom) // 只写
const [value, setValue] = useAtom(countAtom) // 读写
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// 挂载副作用原子而不读取值
function App() {
useAtomMount(keyboardShortcutsAtom)
return <>{children}</>
}
使用 Result.builder 处理结果
使用 Result.builder 用于渲染效果原子结果。它提供具有 onErrorTag 的可链式错误处理:
import { Result } from "@effect-atom/atom-react"
function UserProfile() {
const userResult = useAtomValue(userAtom) // Result<User, Error>
return Result.builder(userResult)
.onInitial(() => <div>Loading...</div>)
.onErrorTag("NotFoundError", () => <div>User not found</div>)
.onError((error) => <div>Error: {error.message}</div>)
.onSuccess((user) => <div>Hello, {user.name}</div>)
.render()
}
带副作用的原子
const scrollYAtom = Atom.make((get) => {
const onScroll = () => get.setSelf(window.scrollY)
window.addEventListener("scroll", onScroll)
get.addFinalizer(() => window.removeEventListener("scroll", onScroll)) // 必需
return window.scrollY
}).pipe(Atom.keepAlive)
参见 references/effect-atom-patterns.md 获取完整模式,包括家族、localStorage 和反模式。
RPC & 集群模式
对于RPC契约和集群工作流,参见:
references/rpc-cluster-patterns.md- RpcGroup, Workflow.make, Activity 模式
反模式(禁止)
这些模式永远不可接受:
// 禁止 - 在服务内 runSync/runPromise
const result = Effect.runSync(someEffect) // 永远不要这样做
// 禁止 - 在 Effect.gen 内 throw
yield* Effect.gen(function* () {
if (bad) throw new Error("No!") // 使用 Effect.fail 代替
})
// 禁止 - catchAll 丢失类型信息
yield* effect.pipe(Effect.catchAll(() => Effect.fail(new GenericError())))
// 禁止 - console.log
console.log("debug") // 使用 Effect.log
// 禁止 - 直接使用 process.env
const key = process.env.API_KEY // 使用 Config.string("API_KEY")
// 禁止 - 在域类型中使用 null/undefined
type User = { name: string | null } // 使用 Option<string>
参见 references/anti-patterns.md 获取完整列表及理由。
可观察性
// 结构化日志
yield* Effect.log("Processing order", { orderId, userId, amount })
// 指标
const orderCounter = Metric.counter("orders_processed")
yield* Metric.increment(orderCounter)
// 配置带验证
const config = Config.all({
port: Config.integer("PORT").pipe(Config.withDefault(3000)),
apiKey: Config.secret("API_KEY"),
maxRetries: Config.integer("MAX_RETRIES").pipe(
Config.validate({ message: "Must be positive", validation: (n) => n > 0 })
),
})
参见 references/observability-patterns.md 获取指标和跟踪模式。
参考文件
对于详细模式,请查阅 references/ 目录中的这些参考文件:
service-patterns.md- 服务定义, Effect.fn, Context.Tag 例外error-patterns.md- Schema.TaggedError, 错误重映射, 重试模式schema-patterns.md- 品牌类型, 转换, Schema.Classlayer-patterns.md- 依赖组合, 测试层rpc-cluster-patterns.md- RpcGroup, Workflow, Activity 模式effect-atom-patterns.md- 原子, 家族, React 钩子, 结果处理anti-patterns.md- 禁止模式的完整列表observability-patterns.md- 日志, 指标, 配置模式