effective-effect

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Effect-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 value
Effect<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
NotFoundError
or
BadRequestError
.
typescript
// 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:
  • UserNotFoundError
    with
    userId
    → Frontend shows "User doesn't exist"
  • SessionExpiredError
    with
    expiredAt
    → Frontend shows "Session expired, please log in"
  • Type-safe error handling with
    catchTag
    /
    catchTags
  • Not using the
    message
    field allows
    Effect.log
    to log all error context values. This way we will see both the
    expiredAt
    and
    userMessage
    fields in the logs, if we log the error.
  • Using a
    cause
    field of type
    Schema.Defect
    preserves the original stack trace when logging the error.
每个不同的失败原因都应该有对应的专属错误类型。 不要将多种失败模式合并为
NotFoundError
BadRequestError
这类通用HTTP错误。
typescript
// 错误示例 - 通用错误会丢失上下文
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

快速参考:核心规则

CategoryDODON'T
Services (app)
Effect.Service
with default implementation
Inline dependencies in methods
Services (lib)
Context.Tag
when no sensible default exists
Assuming implementation in library code
Dependencies
dependencies: [...]
or yield in Layer
Pass services as function parameters
Errors
Schema.TaggedError
(yieldable)
Plain classes or generic Error
Error Recovery
catchTag
/
catchTags
with pattern matching
catchAll
losing type info
IDs
Schema.String.pipe(Schema.brand("UserId"))
Plain
string
for entity IDs
Functions
Effect.fn("Service.method")
Anonymous generators
Sequencing
Effect.gen
with
yield*
Nested
.then()
or
.pipe()
chains
Logging
Effect.log
with structured data
console.log
Config
Schema.Config
or
Config.*
primitives
process.env
directly
Options
Option.match
with both cases
Option.getOrThrow
Nullability
Option<T>
in domain types
null
/
undefined
Test Layers
Layer.sync
with in-memory state
Mocking frameworks
Atoms
Atom.make
outside components
Creating atoms inside render
Atom Results
Result.builder
with
onErrorTag
Ignoring loading/error states
分类正确做法错误做法
服务(应用代码)使用带默认实现的
Effect.Service
在方法中直接嵌入依赖项
服务(库代码)无合理默认实现时使用
Context.Tag
在库代码中假设具体实现
依赖项管理在Layer中使用
dependencies: [...]
或yield语法
将服务作为函数参数传递
错误定义使用
Schema.TaggedError
(可yield的错误类型)
使用普通类或通用Error类型
错误恢复使用
catchTag
/
catchTags
进行模式匹配
使用
catchAll
丢失类型信息
实体ID使用
Schema.String.pipe(Schema.brand("UserId"))
对实体ID使用普通
string
类型
函数定义使用
Effect.fn("Service.method")
使用匿名生成器函数
流程编排使用
Effect.gen
配合
yield*
使用嵌套的
.then()
.pipe()
链式调用
日志记录使用
Effect.log
记录结构化数据
使用
console.log
配置管理使用
Schema.Config
Config.*
原语
直接使用
process.env
可选值处理使用
Option.match
处理两种情况
使用
Option.getOrThrow
空值处理在领域类型中使用
Option<T>
使用
null
/
undefined
测试层使用
Layer.sync
创建内存状态的测试层
使用Mocking框架
原子状态在组件外部使用
Atom.make
创建
在渲染函数内部创建原子状态
原子结果处理使用
Result.builder
配合
onErrorTag
忽略加载/错误状态

Basics

基础用法

Effect.gen

Effect.gen

Just as
async/await
provides a sequential, readable way to work with
Promise
values,
Effect.gen
and
yield*
provide the same ergonomic benefits for
Effect
values:
typescript
import { Effect } from "effect";

const program = Effect.gen(function* () {
  const data = yield* fetchData;
  yield* Effect.logInfo(`Processing data: ${data}`);
  return yield* processData(data);
});
就像
async/await
Promise
值提供了一种顺序化、可读性强的处理方式一样,
Effect.gen
yield*
也为
Effect
值带来了同样舒适的使用体验:
typescript
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
Effect.fn
with generator functions for traced, named effects.
Effect.fn
traces where the function is called from, not just where it's defined:
typescript
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.fn
与生成器函数配合使用,可创建带追踪信息、有明确名称的Effect。
Effect.fn
会追踪函数的调用位置,而不仅仅是定义位置:
typescript
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
.pipe()
to add cross-cutting concerns to Effect values:
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:
  • Effect.timeout
    - fail if effect takes too long
  • Effect.retry
    - retry on failure with a schedule
  • Effect.tap
    - run side effect without changing the value
  • Effect.withSpan
    - add tracing span
使用
.pipe()
为Effect值添加横切关注点:
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.timeout
    - 若Effect执行超时则标记为失败
  • Effect.retry
    - 按指定策略在失败时重试
  • Effect.tap
    - 执行副作用但不修改原返回值
  • Effect.withSpan
    - 添加追踪Span

Service Definition Pattern

服务定义模式

Effect provides two ways to model services:
Effect.Service
and
Context.Tag
. Choose based on your use case:
FeatureEffect.ServiceContext.Tag
Best forApplication code with clear implementationLibrary code or dynamically-scoped values
Default implRequired (becomes
.Default
layer)
Optional - supplied later
BoilerplateLess - tag + layer generatedMore - build layers yourself
Effect提供两种服务建模方式:
Effect.Service
Context.Tag
,需根据使用场景选择:
特性Effect.ServiceContext.Tag
最佳适用场景有明确实现的应用代码库代码或动态作用域的值
默认实现要求必填(会生成为
.Default
层)
可选 - 后续提供即可
样板代码量较少 - 自动生成tag和层较多 - 需要手动构建层

Effect.Service (Preferred for App Code)

Effect.Service(应用代码优先选择)

Use
Effect.Service
when you have a sensible default implementation:
typescript
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.Service
typescript
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
Context.Tag
when no sensible default exists or you're writing library code:
typescript
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
    @path/to/ServiceName
    prefix pattern
  • Service methods should have no dependencies (
    R = never
    )
  • Use readonly properties
See
references/service-patterns.md
for service-driven development and test layers.
当无合理默认实现或编写库代码时,使用
Context.Tag
typescript
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.md

Error Definition Pattern

错误定义模式

Use
Schema.TaggedError
for errors. They are serializable (required for RPC) and yieldable (no need for
Effect.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;
});
使用
Schema.TaggedError
定义错误,它们支持序列化(RPC场景必需)且可yield
(无需使用
Effect.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
catchTag
/
catchTags
for type-safe error handling:
typescript
// 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 }),
    }),
  );
使用
catchTag
/
catchTags
进行类型安全的错误处理:
typescript
// 处理单一错误类型
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.Defect
:
typescript
class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
  endpoint: Schema.String,
  statusCode: Schema.Number,
  error: Schema.Defect, // Wraps unknown errors
}) {}
See
references/error-patterns.md
for expected vs defects and retry patterns.
Schema.Defect
包装外部库的错误:
typescript
class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
  endpoint: Schema.String,
  statusCode: Schema.Number,
  error: Schema.Defect, // 包装未知错误
}) {}
如需了解预期错误与缺陷错误的区别以及重试模式,请查阅
references/error-patterns.md

Data 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
Schema.Class
for composite data models:
typescript
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.Class
建模复合数据模型:
typescript
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
Schema.TaggedClass
with
Schema.Union
for discriminated unions:
typescript
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
references/schema-patterns.md
for JSON encoding and advanced patterns.
使用
Schema.TaggedClass
配合
Schema.Union
建模可区分联合类型:
typescript
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.md

Layer 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
references/layer-patterns.md
for test layers and config-dependent layers.
Effect通过引用标识对层进行记忆化,参数化的层需存储在常量中:
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.md

Config

配置管理

Use
Config.*
primitives or
Schema.Config
for type-safe configuration:
typescript
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.Config
实现类型安全的配置管理:
typescript
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
references/config-patterns.md
for ConfigProvider and advanced patterns.
创建带测试层的配置服务:
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.md

Testing

测试

Use
@effect/vitest
for Effect-native testing:
typescript
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/vitest
进行Effect原生测试:
typescript
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.sync
:
typescript
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
references/testing-patterns.md
for TestClock and worked examples.
使用
Layer.sync
创建内存状态的测试层:
typescript
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.md

CLI

CLI开发

Use
@effect/cli
for typed argument parsing:
typescript
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
references/cli-patterns.md
for subcommands and service integration.
使用
@effect/cli
实现类型安全的参数解析:
typescript
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.md

Effect 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
references/effect-atom-patterns.md
for complete patterns.
typescript
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.md

Anti-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
references/anti-patterns.md
for the complete list with rationale.
typescript
// 禁止 - 在服务内部使用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.md

Reference Files / How to use

参考文档 / 使用方式

For detailed patterns or in the case of any ambiguity you must consult these reference files in the
references/
directory:
  • anti-patterns.md
    - Complete list of forbidden patterns
  • cli-patterns.md
    - @effect/cli Commands, Args, Options, subcommands
  • config-patterns.md
    - Config primitives, Schema.Config, ConfigProvider
  • domain-predicates.md
    - Equivalence, Order, Schema.Data for equality and sorting
  • effect-atom-patterns.md
    - Atom, families, React hooks, Result handling
  • error-patterns.md
    - Schema.TaggedError, yieldable errors, Schema.Defect
  • layer-patterns.md
    - Dependency composition, memoization, testing layers
  • observability-patterns.md
    - Logging, metrics, config patterns
  • rpc-cluster-patterns.md
    - RpcGroup, Workflow, Activity patterns
  • schema-patterns.md
    - Branded types, Schema.Class, JSON encoding
  • service-patterns.md
    - Effect.Service vs Context.Tag, dependencies, test layers
  • testing-patterns.md
    - @effect/vitest, it.effect, it.scoped, TestClock, property-based testing
若需了解详细模式或遇到歧义,必须查阅
references/
目录下的以下参考文档:
  • anti-patterns.md
    - 完整的禁止模式列表
  • cli-patterns.md
    - @effect/cli的命令、参数、选项、子命令相关内容
  • config-patterns.md
    - Config原语、Schema.Config、ConfigProvider相关内容
  • domain-predicates.md
    - 用于相等性判断和排序的Equivalence、Order、Schema.Data相关内容
  • effect-atom-patterns.md
    - Atom、原子家族、React钩子、结果处理相关内容
  • error-patterns.md
    - Schema.TaggedError、可yield错误、Schema.Defect相关内容
  • layer-patterns.md
    - 依赖组合、记忆化、测试层相关内容
  • observability-patterns.md
    - 日志、指标、配置模式相关内容
  • rpc-cluster-patterns.md
    - RpcGroup、Workflow、Activity模式相关内容
  • schema-patterns.md
    - 品牌化类型、Schema.Class、JSON编码相关内容
  • service-patterns.md
    - Effect.Service与Context.Tag对比、依赖管理、测试层相关内容
  • testing-patterns.md
    - @effect/vitest、it.effect、it.scoped、TestClock、属性化测试相关内容