effective-effect
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEffect-TS Best Practices
Effect-TS 最佳实践
This skill enforces opinionated, consistent patterns for Effect-TS codebases. These patterns optimize for type safety, testability, observability, and maintainability.
本规范为Effect-TS代码库强制推行一套有明确立场、保持一致的编码模式,这些模式可优化代码的类型安全性、可测试性、可观测性和可维护性。
Core Principles
核心原则
Effect Type Signature
Effect 类型签名
Effect<Success, Error, Requirements>
// ↑ ↑ ↑
// | | └── Dependencies (provided via Layers)
// | └── Expected errors (typed, must be handled)
// └── Success valueEffect<Success, Error, Requirements>
// ↑ ↑ ↑
// | | └── 依赖项(通过Layer提供)
// | └── 预期错误(已类型化,必须处理)
// └── 成功返回值Prefer Explicit Over Generic Errors
优先使用明确错误而非通用错误
Every distinct failure reason deserves its own error type. Don't collapse multiple failure modes into generic HTTP errors like or .
NotFoundErrorBadRequestErrortypescript
// BAD - Generic errors lose context
class NotFoundError extends Schema.TaggedError<NotFoundError>()("NotFoundError", {
message: Schema.String, // Dont use `message` as it may hide context when using Effect.log
}) {}
// GOOD - Specific errors enable precise handling
class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()("UserNotFoundError", {
userId: UserId,
userMessage: Schema.String,
}) {}
class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()("SessionExpiredError", {
expiredAt: Schema.Date,
userMessage: Schema.String,
}) {}
// GOOD - Wrapping errors with cause preserves stack traces
class UserLookupError extends Schema.TaggedError<UserLookupError>()("UserLookupError", {
userId: UserId,
reason: Schema.String, // prefer using `reason` over `message` for logging
cause: Schema.Defect, // Wraps the underlying error - Effect.log prints as stack trace
}) {}Benefits:
- with
UserNotFoundError→ Frontend shows "User doesn't exist"userId - with
SessionExpiredError→ Frontend shows "Session expired, please log in"expiredAt - Type-safe error handling with /
catchTagcatchTags - Not using the field allows
messageto log all error context values. This way we will see both theEffect.logandexpiredAtfields in the logs, if we log the error.userMessage - Using a field of type
causepreserves the original stack trace when logging the error.Schema.Defect
每个不同的失败原因都应该有对应的专属错误类型。 不要将多种失败模式合并为或这类通用HTTP错误。
NotFoundErrorBadRequestErrortypescript
// 错误示例 - 通用错误会丢失上下文
class NotFoundError extends Schema.TaggedError<NotFoundError>()("NotFoundError", {
message: Schema.String, // 不要使用`message`字段,因为使用Effect.log时可能会隐藏上下文信息
}) {}
// 正确示例 - 明确错误支持精准处理
class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()("UserNotFoundError", {
userId: UserId,
userMessage: Schema.String,
}) {}
class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()("SessionExpiredError", {
expiredAt: Schema.Date,
userMessage: Schema.String,
}) {}
// 正确示例 - 用cause包装错误可保留堆栈跟踪
class UserLookupError extends Schema.TaggedError<UserLookupError>()("UserLookupError", {
userId: UserId,
reason: Schema.String, // 日志记录时优先使用`reason`而非`message`
cause: Schema.Defect, // 包装底层错误 - Effect.log会将其打印为堆栈跟踪
}) {}优势:
- 携带的
userId→ 前端可展示“用户不存在”UserNotFoundError - 携带的
expiredAt→ 前端可展示“会话已过期,请重新登录”SessionExpiredError - 可使用/
catchTag进行类型安全的错误处理catchTags - 不使用字段可让
message记录所有错误上下文信息,这样在记录错误时,我们能同时看到Effect.log和expiredAt字段userMessage - 使用类型为的
Schema.Defect字段可在记录错误时保留原始堆栈跟踪cause
Quick Reference: Critical Rules
快速参考:核心规则
| Category | DO | DON'T |
|---|---|---|
| Services (app) | | Inline dependencies in methods |
| Services (lib) | | Assuming implementation in library code |
| Dependencies | | Pass services as function parameters |
| Errors | | Plain classes or generic Error |
| Error Recovery | | |
| IDs | | Plain |
| Functions | | Anonymous generators |
| Sequencing | | Nested |
| Logging | | |
| Config | | |
| Options | | |
| Nullability | | |
| Test Layers | | Mocking frameworks |
| Atoms | | Creating atoms inside render |
| Atom Results | | Ignoring loading/error states |
| 分类 | 正确做法 | 错误做法 |
|---|---|---|
| 服务(应用代码) | 使用带默认实现的 | 在方法中直接嵌入依赖项 |
| 服务(库代码) | 无合理默认实现时使用 | 在库代码中假设具体实现 |
| 依赖项管理 | 在Layer中使用 | 将服务作为函数参数传递 |
| 错误定义 | 使用 | 使用普通类或通用Error类型 |
| 错误恢复 | 使用 | 使用 |
| 实体ID | 使用 | 对实体ID使用普通 |
| 函数定义 | 使用 | 使用匿名生成器函数 |
| 流程编排 | 使用 | 使用嵌套的 |
| 日志记录 | 使用 | 使用 |
| 配置管理 | 使用 | 直接使用 |
| 可选值处理 | 使用 | 使用 |
| 空值处理 | 在领域类型中使用 | 使用 |
| 测试层 | 使用 | 使用Mocking框架 |
| 原子状态 | 在组件外部使用 | 在渲染函数内部创建原子状态 |
| 原子结果处理 | 使用 | 忽略加载/错误状态 |
Basics
基础用法
Effect.gen
Effect.gen
Just as provides a sequential, readable way to work with values, and provide the same ergonomic benefits for values:
async/awaitPromiseEffect.genyield*Effecttypescript
import { Effect } from "effect";
const program = Effect.gen(function* () {
const data = yield* fetchData;
yield* Effect.logInfo(`Processing data: ${data}`);
return yield* processData(data);
});就像为值提供了一种顺序化、可读性强的处理方式一样,和也为值带来了同样舒适的使用体验:
async/awaitPromiseEffect.genyield*Effecttypescript
import { Effect } from "effect";
const program = Effect.gen(function* () {
const data = yield* fetchData;
yield* Effect.logInfo(`Processing data: ${data}`);
return yield* processData(data);
});Effect.fn
Effect.fn
Use with generator functions for traced, named effects. traces where the function is called from, not just where it's defined:
Effect.fnEffect.fntypescript
import { Effect } from "effect";
const processUser = Effect.fn("processUser")(function* (userId: string) {
yield* Effect.logInfo(`Processing user ${userId}`);
const user = yield* getUser(userId);
return yield* processData(user);
});Benefits:
- Call-site tracing for each invocation
- Stack traces with location details
- Clean signatures
- Automatic spans for telemetry
将与生成器函数配合使用,可创建带追踪信息、有明确名称的Effect。会追踪函数的调用位置,而不仅仅是定义位置:
Effect.fnEffect.fntypescript
import { Effect } from "effect";
const processUser = Effect.fn("processUser")(function* (userId: string) {
yield* Effect.logInfo(`Processing user ${userId}`);
const user = yield* getUser(userId);
return yield* processData(user);
});优势:
- 为每个调用生成调用位置追踪信息
- 包含位置详情的堆栈跟踪
- 清晰的函数签名
- 自动生成遥测用的Span
Pipe for Instrumentation
使用Pipe实现可观测性增强
Use to add cross-cutting concerns to Effect values:
.pipe()typescript
import { Effect, Schedule } from "effect";
const program = fetchData.pipe(
Effect.timeout("5 seconds"),
Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.compose(Schedule.recurs(3)))),
Effect.tap((data) => Effect.logInfo(`Fetched: ${data}`)),
Effect.withSpan("fetchData"),
);Common instrumentation:
- - fail if effect takes too long
Effect.timeout - - retry on failure with a schedule
Effect.retry - - run side effect without changing the value
Effect.tap - - add tracing span
Effect.withSpan
使用为Effect值添加横切关注点:
.pipe()typescript
import { Effect, Schedule } from "effect";
const program = fetchData.pipe(
Effect.timeout("5 seconds"),
Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.compose(Schedule.recurs(3)))),
Effect.tap((data) => Effect.logInfo(`Fetched: ${data}`)),
Effect.withSpan("fetchData"),
);常见的可观测性增强操作:
- - 若Effect执行超时则标记为失败
Effect.timeout - - 按指定策略在失败时重试
Effect.retry - - 执行副作用但不修改原返回值
Effect.tap - - 添加追踪Span
Effect.withSpan
Service Definition Pattern
服务定义模式
Effect provides two ways to model services: and . Choose based on your use case:
Effect.ServiceContext.Tag| Feature | Effect.Service | Context.Tag |
|---|---|---|
| Best for | Application code with clear implementation | Library code or dynamically-scoped values |
| Default impl | Required (becomes | Optional - supplied later |
| Boilerplate | Less - tag + layer generated | More - build layers yourself |
Effect提供两种服务建模方式:和,需根据使用场景选择:
Effect.ServiceContext.Tag| 特性 | Effect.Service | Context.Tag |
|---|---|---|
| 最佳适用场景 | 有明确实现的应用代码 | 库代码或动态作用域的值 |
| 默认实现要求 | 必填(会生成为 | 可选 - 后续提供即可 |
| 样板代码量 | 较少 - 自动生成tag和层 | 较多 - 需要手动构建层 |
Effect.Service (Preferred for App Code)
Effect.Service(应用代码优先选择)
Use when you have a sensible default implementation:
Effect.Servicetypescript
import { Effect } from "effect";
export class UserService extends Effect.Service<UserService>()("@app/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 };
}),
}) {}
// Usage - dependencies automatically wired via .Default
const program = Effect.gen(function* () {
const user = yield* UserService.findById(userId); // accessors enabled
});
// At app root
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default);The class is the tag: You can provide alternate implementations for testing:
typescript
const mock = new UserService({ findById: () => Effect.succeed(mockUser) });
program.pipe(Effect.provideService(UserService, mock));当有合理的默认实现时,使用:
Effect.Servicetypescript
import { Effect } from "effect";
export class UserService extends Effect.Service<UserService>()("@app/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 };
}),
}) {}
// 使用方式 - 依赖项通过.Default自动注入
const program = Effect.gen(function* () {
const user = yield* UserService.findById(userId); // 已启用访问器
});
// 在应用根节点
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default);类本身就是tag: 你可以为测试提供替代实现:
typescript
const mock = new UserService({ findById: () => Effect.succeed(mockUser) });
program.pipe(Effect.provideService(UserService, mock));Context.Tag (For Libraries / No Default)
Context.Tag(适用于库代码/无默认实现场景)
Use when no sensible default exists or you're writing library code:
Context.Tagtypescript
import { Context, Effect, Layer } from "effect";
// Per-request database handle - no sensible global default
class RequestDb extends Context.Tag("@app/RequestDb")<
RequestDb,
{ readonly query: (sql: string) => Effect.Effect<unknown[]> }
>() {}
// Library code - callers provide implementation
class PaymentGateway extends Context.Tag("@lib/PaymentGateway")<
PaymentGateway,
{ readonly charge: (amount: number) => Effect.Effect<Receipt, PaymentError> }
>() {}
// Implement with Layer.effect when needed
const RequestDbLive = Layer.effect(
RequestDb,
Effect.gen(function* () {
const pool = yield* DatabasePool;
return RequestDb.of({
query: (sql) => pool.query(sql),
});
}),
);Key rules:
- Tag identifiers must be unique. Use prefix pattern
@path/to/ServiceName - Service methods should have no dependencies ()
R = never - Use readonly properties
See for service-driven development and test layers.
references/service-patterns.md当无合理默认实现或编写库代码时,使用:
Context.Tagtypescript
import { Context, Effect, Layer } from "effect";
// 每个请求的数据库句柄 - 无合理全局默认实现
class RequestDb extends Context.Tag("@app/RequestDb")<
RequestDb,
{ readonly query: (sql: string) => Effect.Effect<unknown[]> }
>() {}
// 库代码 - 由调用方提供具体实现
class PaymentGateway extends Context.Tag("@lib/PaymentGateway")<
PaymentGateway,
{ readonly charge: (amount: number) => Effect.Effect<Receipt, PaymentError> }
>() {}
// 需要时用Layer.effect实现
const RequestDbLive = Layer.effect(
RequestDb,
Effect.gen(function* () {
const pool = yield* DatabasePool;
return RequestDb.of({
query: (sql) => pool.query(sql),
});
}),
);核心规则:
- Tag标识符必须唯一,使用前缀模式
@path/to/ServiceName - 服务方法应无依赖()
R = never - 使用只读属性
如需了解服务驱动开发和测试层的更多内容,请查阅。
references/service-patterns.mdError Definition Pattern
错误定义模式
Use for errors. They are serializable (required for RPC) and yieldable (no need for ):
Schema.TaggedErrorEffect.fail()typescript
import { Schema } from "effect";
class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()("UserNotFoundError", {
userId: UserId,
userMessage: Schema.String,
}) {}
// Usage - yieldable errors can be used directly
const findUser = Effect.fn("findUser")(function* (id: UserId) {
const user = yield* repo.findById(id);
if (Option.isNone(user)) {
return yield* UserNotFoundError.make({ userId: id, userMessage: "User not found" });
}
return user.value;
});使用定义错误,它们支持序列化(RPC场景必需)且可yield(无需使用):
Schema.TaggedErrorEffect.fail()typescript
import { Schema } from "effect";
class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()("UserNotFoundError", {
userId: UserId,
userMessage: Schema.String,
}) {}
// 使用方式 - 可yield的错误可直接使用
const findUser = Effect.fn("findUser")(function* (id: UserId) {
const user = yield* repo.findById(id);
if (Option.isNone(user)) {
return yield* UserNotFoundError.make({ userId: id, userMessage: "User not found" });
}
return user.value;
});Error Recovery
错误恢复
Use / for type-safe error handling:
catchTagcatchTagstypescript
// Single error type
yield *
repo
.findById(id)
.pipe(
Effect.catchTag("DatabaseError", (err) =>
UserLookupError.make({ userId: id, reason: err.reason, cause: err }),
),
);
// Multiple error types
yield *
effect.pipe(
Effect.catchTags({
DatabaseError: (err) => UserLookupError.make({ userId: id, reason: err.reason, cause: err }),
ValidationError: (err) =>
InvalidInputError.make({ field: err.field, reason: err.reason, cause: err }),
}),
);使用/进行类型安全的错误处理:
catchTagcatchTagstypescript
// 处理单一错误类型
yield *
repo
.findById(id)
.pipe(
Effect.catchTag("DatabaseError", (err) =>
UserLookupError.make({ userId: id, reason: err.reason, cause: err }),
),
);
// 处理多种错误类型
yield *
effect.pipe(
Effect.catchTags({
DatabaseError: (err) => UserLookupError.make({ userId: id, reason: err.reason, cause: err }),
ValidationError: (err) =>
InvalidInputError.make({ field: err.field, reason: err.reason, cause: err }),
}),
);Schema.Defect for Unknown Errors
使用Schema.Defect处理未知错误
Wrap errors from external libraries with :
Schema.Defecttypescript
class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
endpoint: Schema.String,
statusCode: Schema.Number,
error: Schema.Defect, // Wraps unknown errors
}) {}See for expected vs defects and retry patterns.
references/error-patterns.md用包装外部库的错误:
Schema.Defecttypescript
class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
endpoint: Schema.String,
statusCode: Schema.Number,
error: Schema.Defect, // 包装未知错误
}) {}如需了解预期错误与缺陷错误的区别以及重试模式,请查阅。
references/error-patterns.mdData Modeling
数据建模
Branded Types
品牌化类型
Brand all entity IDs to prevent mixing values with the same underlying type:
typescript
import { Schema } from "effect";
export const UserId = Schema.String.pipe(Schema.brand("UserId"));
export type UserId = typeof UserId.Type;
export const PostId = Schema.String.pipe(Schema.brand("PostId"));
export type PostId = typeof PostId.Type;
// Type error: can't pass PostId where UserId expected
function getUser(id: UserId) {
/* ... */
}
getUser(PostId.make("post-123")); // Error!为所有实体ID添加品牌化标记,防止混用底层类型相同但语义不同的值:
typescript
import { Schema } from "effect";
export const UserId = Schema.String.pipe(Schema.brand("UserId"));
export type UserId = typeof UserId.Type;
export const PostId = Schema.String.pipe(Schema.brand("PostId"));
export type PostId = typeof PostId.Type;
// 类型错误:不能将PostId传入需要UserId的函数
function getUser(id: UserId) {
/* ... */
}
getUser(PostId.make("post-123")); // 错误!Schema.Class for Records
使用Schema.Class建模记录类型
Use for composite data models:
Schema.Classtypescript
export class User extends Schema.Class<User>("User")({
id: UserId,
name: Schema.String,
email: Schema.String,
createdAt: Schema.Date,
}) {
get displayName() {
return `${this.name} (${this.email})`;
}
}使用建模复合数据模型:
Schema.Classtypescript
export class User extends Schema.Class<User>("User")({
id: UserId,
name: Schema.String,
email: Schema.String,
createdAt: Schema.Date,
}) {
get displayName() {
return `${this.name} (${this.email})`;
}
}Schema.TaggedClass for Variants
使用Schema.TaggedClass建模变体类型
Use with for discriminated unions:
Schema.TaggedClassSchema.Uniontypescript
import { Match, Schema } from "effect";
export class Success extends Schema.TaggedClass<Success>()("Success", {
value: Schema.Number,
}) {}
export class Failure extends Schema.TaggedClass<Failure>()("Failure", {
error: Schema.String,
}) {}
export const Result = Schema.Union(Success, Failure);
// Pattern match with Match.valueTags
Match.valueTags(result, {
Success: ({ value }) => `Got: ${value}`,
Failure: ({ error }) => `Error: ${error}`,
});See for JSON encoding and advanced patterns.
references/schema-patterns.md使用配合建模可区分联合类型:
Schema.TaggedClassSchema.Uniontypescript
import { Match, Schema } from "effect";
export class Success extends Schema.TaggedClass<Success>()("Success", {
value: Schema.Number,
}) {}
export class Failure extends Schema.TaggedClass<Failure>()("Failure", {
error: Schema.String,
}) {}
export const Result = Schema.Union(Success, Failure);
// 使用Match.valueTags进行模式匹配
Match.valueTags(result, {
Success: ({ value }) => `得到结果:${value}`,
Failure: ({ error }) => `错误:${error}`,
});如需了解JSON编码和高级模式,请查阅。
references/schema-patterns.mdLayer Composition & Memoization
Layer组合与记忆化
Provide layers once at the top of your application:
typescript
const appLayer = userServiceLayer.pipe(
Layer.provideMerge(databaseLayer),
Layer.provideMerge(loggerLayer),
);
const main = program.pipe(Effect.provide(appLayer));
Effect.runPromise(main);在应用最顶层一次性提供所有层:
typescript
const appLayer = userServiceLayer.pipe(
Layer.provideMerge(databaseLayer),
Layer.provideMerge(loggerLayer),
);
const main = program.pipe(Effect.provide(appLayer));
Effect.runPromise(main);Layer Memoization Warning
Layer记忆化注意事项
Effect memoizes layers by reference identity. Store parameterized layers in constants:
typescript
// BAD: creates TWO connection pools
const badLayer = Layer.merge(
UserRepo.layer.pipe(Layer.provide(Postgres.layer({ url: "..." }))),
OrderRepo.layer.pipe(Layer.provide(Postgres.layer({ url: "..." }))), // Different reference!
);
// GOOD: single connection pool
const postgresLayer = Postgres.layer({ url: "..." });
const goodLayer = Layer.merge(
UserRepo.layer.pipe(Layer.provide(postgresLayer)),
OrderRepo.layer.pipe(Layer.provide(postgresLayer)), // Same reference!
);See for test layers and config-dependent layers.
references/layer-patterns.mdEffect通过引用标识对层进行记忆化,参数化的层需存储在常量中:
typescript
// 错误示例:会创建两个连接池
const badLayer = Layer.merge(
UserRepo.layer.pipe(Layer.provide(Postgres.layer({ url: "..." }))),
OrderRepo.layer.pipe(Layer.provide(Postgres.layer({ url: "..." }))), // 引用不同!
);
// 正确示例:仅创建一个连接池
const postgresLayer = Postgres.layer({ url: "..." });
const goodLayer = Layer.merge(
UserRepo.layer.pipe(Layer.provide(postgresLayer)),
OrderRepo.layer.pipe(Layer.provide(postgresLayer)), // 引用相同!
);如需了解测试层和依赖配置的层,请查阅。
references/layer-patterns.mdConfig
配置管理
Use primitives or for type-safe configuration:
Config.*Schema.Configtypescript
import { Config, Effect, Schema } from "effect";
const Port = Schema.Int.pipe(Schema.between(1, 65535));
const program = Effect.gen(function* () {
// Basic primitives
const apiKey = yield* Config.redacted("API_KEY");
const port = yield* Config.integer("PORT");
// With Schema validation
const validatedPort = yield* Schema.Config("PORT", Port);
});使用原语或实现类型安全的配置管理:
Config.*Schema.Configtypescript
import { Config, Effect, Schema } from "effect";
const Port = Schema.Int.pipe(Schema.between(1, 65535));
const program = Effect.gen(function* () {
// 基础原语类型
const apiKey = yield* Config.redacted("API_KEY");
const port = yield* Config.integer("PORT");
// 配合Schema验证
const validatedPort = yield* Schema.Config("PORT", Port);
});Config Service Pattern
配置服务模式
Create config services with test layers:
typescript
class ApiConfig extends Context.Tag("@app/ApiConfig")<
ApiConfig,
{ readonly apiKey: Redacted.Redacted; readonly baseUrl: string }
>() {
static readonly layer = Layer.effect(
ApiConfig,
Effect.gen(function* () {
const apiKey = yield* Config.redacted("API_KEY");
const baseUrl = yield* Config.string("API_BASE_URL");
return ApiConfig.of({ apiKey, baseUrl });
}),
);
// For tests - hardcoded values
static readonly testLayer = Layer.succeed(ApiConfig, {
apiKey: Redacted.make("test-key"),
baseUrl: "https://test.example.com",
});
}See for ConfigProvider and advanced patterns.
references/config-patterns.md创建带测试层的配置服务:
typescript
class ApiConfig extends Context.Tag("@app/ApiConfig")<
ApiConfig,
{ readonly apiKey: Redacted.Redacted; readonly baseUrl: string }
>() {
static readonly layer = Layer.effect(
ApiConfig,
Effect.gen(function* () {
const apiKey = yield* Config.redacted("API_KEY");
const baseUrl = yield* Config.string("API_BASE_URL");
return ApiConfig.of({ apiKey, baseUrl });
}),
);
// 测试用 - 硬编码值
static readonly testLayer = Layer.succeed(ApiConfig, {
apiKey: Redacted.make("test-key"),
baseUrl: "https://test.example.com",
});
}如需了解ConfigProvider和高级模式,请查阅。
references/config-patterns.mdTesting
测试
Use for Effect-native testing:
@effect/vitesttypescript
import { Effect } from "effect";
import { describe, expect, it } from "@effect/vitest";
describe("Calculator", () => {
it.effect("adds numbers", () =>
Effect.gen(function* () {
const result = yield* Effect.succeed(1 + 1);
expect(result).toBe(2);
}),
);
// With scoped resources
it.scoped("cleans up resources", () =>
Effect.gen(function* () {
const tempDir = yield* fs.makeTempDirectoryScoped();
// tempDir deleted when scope closes
}),
);
});使用进行Effect原生测试:
@effect/vitesttypescript
import { Effect } from "effect";
import { describe, expect, it } from "@effect/vitest";
describe("Calculator", () => {
it.effect("加法运算", () =>
Effect.gen(function* () {
const result = yield* Effect.succeed(1 + 1);
expect(result).toBe(2);
}),
);
// 带作用域资源的测试
it.scoped("资源自动清理", () =>
Effect.gen(function* () {
const tempDir = yield* fs.makeTempDirectoryScoped();
// 作用域关闭时tempDir会被自动删除
}),
);
});Test Layers
测试层
Create in-memory test layers with :
Layer.synctypescript
class Users extends Context.Tag("@app/Users")<
Users,
{
/* ... */
}
>() {
static readonly testLayer = Layer.sync(Users, () => {
const store = new Map<UserId, User>();
const create = (user: User) => Effect.sync(() => void store.set(user.id, user));
const findById = (id: UserId) => Effect.fromNullable(store.get(id));
return Users.of({ create, findById });
});
}See for TestClock and worked examples.
references/testing-patterns.md使用创建内存状态的测试层:
Layer.synctypescript
class Users extends Context.Tag("@app/Users")<
Users,
{
/* ... */
}
>() {
static readonly testLayer = Layer.sync(Users, () => {
const store = new Map<UserId, User>();
const create = (user: User) => Effect.sync(() => void store.set(user.id, user));
const findById = (id: UserId) => Effect.fromNullable(store.get(id));
return Users.of({ create, findById });
});
}如需了解TestClock和示例,请查阅。
references/testing-patterns.mdCLI
CLI开发
Use for typed argument parsing:
@effect/clitypescript
import { Args, Command, Options } from "@effect/cli";
import { BunContext, BunRuntime } from "@effect/platform-bun";
import { Console, Effect } from "effect";
const name = Args.text({ name: "name" }).pipe(Args.withDefault("World"));
const shout = Options.boolean("shout").pipe(Options.withAlias("s"));
const greet = Command.make("greet", { name, shout }, ({ name, shout }) => {
const message = `Hello, ${name}`;
return Console.log(shout ? message.toUpperCase() : message);
});
const cli = Command.run(greet, { name: "greet", version: "1.0.0" });
cli(process.argv).pipe(Effect.provide(BunContext.layer), BunRuntime.runMain);See for subcommands and service integration.
references/cli-patterns.md使用实现类型安全的参数解析:
@effect/clitypescript
import { Args, Command, Options } from "@effect/cli";
import { BunContext, BunRuntime } from "@effect/platform-bun";
import { Console, Effect } from "effect";
const name = Args.text({ name: "name" }).pipe(Args.withDefault("World"));
const shout = Options.boolean("shout").pipe(Options.withAlias("s"));
const greet = Command.make("greet", { name, shout }, ({ name, shout }) => {
const message = `Hello, ${name}`;
return Console.log(shout ? message.toUpperCase() : message);
});
const cli = Command.run(greet, { name: "greet", version: "1.0.0" });
cli(process.argv).pipe(Effect.provide(BunContext.layer), BunRuntime.runMain);如需了解子命令和服务集成,请查阅。
references/cli-patterns.mdEffect Atom (Frontend State)
Effect Atom(前端状态管理)
Effect Atom provides reactive state management for React with Effect integration.
Effect Atom为React提供集成Effect的响应式状态管理方案。
Basic Atoms
基础原子状态
typescript
import { Atom } from "@effect-atom/atom-react";
// Define atoms OUTSIDE components
const countAtom = Atom.make(0);
// Use keepAlive for global state that should persist
const userPrefsAtom = Atom.make({ theme: "dark" }).pipe(Atom.keepAlive);
// Atom families for per-entity state
const modalAtomFamily = Atom.family((type: string) =>
Atom.make({ isOpen: false }).pipe(Atom.keepAlive),
);typescript
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 Integration
React集成
typescript
import { useAtomValue, useAtomSet, useAtom } from "@effect-atom/atom-react"
function Counter() {
const count = useAtomValue(countAtom) // Read only
const setCount = useAtomSet(countAtom) // Write only
const [value, setValue] = useAtom(countAtom) // Read + write
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}typescript
import { useAtomValue, useAtomSet, useAtom } 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>
}Handling Results with Result.builder
使用Result.builder处理结果
typescript
import { Result } from "@effect-atom/atom-react"
function UserProfile() {
const userResult = useAtomValue(userAtom)
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()
}See for complete patterns.
references/effect-atom-patterns.mdtypescript
import { Result } from "@effect-atom/atom-react"
function UserProfile() {
const userResult = useAtomValue(userAtom)
return Result.builder(userResult)
.onInitial(() => <div>加载中...</div>)
.onErrorTag("NotFoundError", () => <div>用户不存在</div>)
.onError((error) => <div>错误:{error.message}</div>)
.onSuccess((user) => <div>你好,{user.name}</div>)
.render()
}如需了解完整模式,请查阅。
references/effect-atom-patterns.mdAnti-Patterns (Forbidden)
反模式(禁止使用)
typescript
// FORBIDDEN - runSync/runPromise inside services
yield *
Effect.gen(function* () {
const result = Effect.runPromise(someEffect);
}); // Always prefer yielding the effect. As a workaround for libraries requiring promises etc, extract the current runtime using `const runtime = yield* Effect.runtime<never>();` then use it to run the promise.
// FORBIDDEN - throw inside Effect.gen
yield *
Effect.gen(function* () {
if (bad) throw new Error("No!"); // Use Effect.fail or yieldable error
});
// FORBIDDEN - catchAll losing type info
yield * effect.pipe(Effect.catchAll(() => Effect.fail(new GenericError())));
// FORBIDDEN - console.log
console.log("debug"); // Use Effect.log
// FORBIDDEN - process.env directly
const key = process.env.API_KEY; // Use Config.string("API_KEY")
// FORBIDDEN - null/undefined in domain types
type User = { name: string | null }; // Use Option<string>See for the complete list with rationale.
references/anti-patterns.mdtypescript
// 禁止 - 在服务内部使用runSync/runPromise
yield *
Effect.gen(function* () {
const result = Effect.runPromise(someEffect);
}); // 始终优先使用yield Effect。若需处理要求Promise的库等情况,可通过`const runtime = yield* Effect.runtime<never>();`获取当前运行时,再用它来执行Promise。
// 禁止 - 在Effect.gen内部抛出错误
yield *
Effect.gen(function* () {
if (bad) throw new Error("No!"); // 请使用Effect.fail或可yield的错误类型
});
// 禁止 - 使用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.mdReference Files / How to use
参考文档 / 使用方式
For detailed patterns or in the case of any ambiguity you must consult these reference files in the directory:
references/- - Complete list of forbidden patterns
anti-patterns.md - - @effect/cli Commands, Args, Options, subcommands
cli-patterns.md - - Config primitives, Schema.Config, ConfigProvider
config-patterns.md - - Equivalence, Order, Schema.Data for equality and sorting
domain-predicates.md - - Atom, families, React hooks, Result handling
effect-atom-patterns.md - - Schema.TaggedError, yieldable errors, Schema.Defect
error-patterns.md - - Dependency composition, memoization, testing layers
layer-patterns.md - - Logging, metrics, config patterns
observability-patterns.md - - RpcGroup, Workflow, Activity patterns
rpc-cluster-patterns.md - - Branded types, Schema.Class, JSON encoding
schema-patterns.md - - Effect.Service vs Context.Tag, dependencies, test layers
service-patterns.md - - @effect/vitest, it.effect, it.scoped, TestClock, property-based testing
testing-patterns.md
若需了解详细模式或遇到歧义,必须查阅目录下的以下参考文档:
references/- - 完整的禁止模式列表
anti-patterns.md - - @effect/cli的命令、参数、选项、子命令相关内容
cli-patterns.md - - Config原语、Schema.Config、ConfigProvider相关内容
config-patterns.md - - 用于相等性判断和排序的Equivalence、Order、Schema.Data相关内容
domain-predicates.md - - Atom、原子家族、React钩子、结果处理相关内容
effect-atom-patterns.md - - Schema.TaggedError、可yield错误、Schema.Defect相关内容
error-patterns.md - - 依赖组合、记忆化、测试层相关内容
layer-patterns.md - - 日志、指标、配置模式相关内容
observability-patterns.md - - RpcGroup、Workflow、Activity模式相关内容
rpc-cluster-patterns.md - - 品牌化类型、Schema.Class、JSON编码相关内容
schema-patterns.md - - Effect.Service与Context.Tag对比、依赖管理、测试层相关内容
service-patterns.md - - @effect/vitest、it.effect、it.scoped、TestClock、属性化测试相关内容
testing-patterns.md