mcpField
E
effect-best-practices

effect-best-practices

github.com/millionco开发3.6k62 次安装30 周前更新
代码仓库

关于 effect-best-practices

为 Effect-TS 代码库制定服务、错误、Schema、状态管理和可观测性方面的一致实践,供开发与维护相关项目的团队使用。

主要功能

  1. 01Effect.Service 与依赖模式
  2. 02标记错误与显式处理
  3. 03品牌化 ID 与 Schema 指引
  4. 04Effect.fn 跟踪与结构化日志
  5. 05React Atom 状态与结果模式

适用场景

  1. 01统一 Effect-TS 项目的服务约定
  2. 02检查错误与 Schema 设计
  3. 03构建由 Effect 支持的 React 状态
  4. 04改进跟踪与结构化日志

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/millionco/expect --skill effect-best-practices

安装后在 Claude Code 中输入/effect-best-practices确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
anti-patterns.md
effect-atom-patterns.md
error-patterns.md
layer-patterns.md
observability-patterns.md
rpc-cluster-patterns.md
schema-patterns.md
service-patterns.md

详细介绍

Effect-TS 最佳实践

此技能为 Effect-TS 代码库规范一致且有明确主张的模式。这些模式可优化类型安全性、可测试性、可观测性和可维护性。

快速参考:关键规则

类别应该做不要做
服务使用带 accessors: true 的 Effect.Service将 Context.Tag 用于业务逻辑
依赖项在服务中使用 dependencies: [Dep.Default]在使用位置手动调用 Layer.provide
错误使用带 message 字段的 Schema.TaggedError使用普通类或通用 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在组件外使用 Atom.make在渲染过程中创建 Atom
Atom 状态对全局状态使用 Atom.keepAlive忘记为持久状态设置 keepAlive
Atom 更新在 React 组件中使用 useAtomSet在 React 中命令式调用 Atom.update
Atom 清理对副作用使用 get.addFinalizer()不清理事件监听器
Atom 结果使用带 onErrorTag 的 Result.builder忽略加载/错误状态

服务定义模式

始终使用 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 };
  }),
}) {}

// Usage - dependencies are already wired
const program = Effect.gen(function* () {
  const user = yield* UserService.findById(userId);
  return user;
});

// At app root
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:

// CORRECT - preserves type information
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" })),
    ),
  );

// CORRECT - multiple tags at once
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 错误。

// WRONG - Generic errors lose information
export class NotFoundError extends Schema.TaggedError<NotFoundError>()(
  "NotFoundError",
  { message: Schema.String },
  HttpApiSchema.annotations({ status: 404 }),
) {}

// Then mapping everything to it:
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" })),
});
// Frontend gets useless: { _tag: "NotFoundError", message: "Not found" }
// Which resource? User? Channel? Message? Can't tell!
// CORRECT - Explicit domain errors with rich context
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 }),
) {}

// Frontend can now show specific UI:
// - UserNotFoundError → "User doesn't exist"
// - ChannelNotFoundError → "Channel was deleted"
// - SessionExpiredError → "Your session expired. Please log in again."

有关错误映射和重试模式,请参阅 references/error-patterns.md。

Schema 和品牌类型模式

为所有实体 ID 添加品牌类型,确保跨服务边界的类型安全:

import { Schema } from "effect";

// Entity IDs - always branded
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>;

// Domain types - use 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>;

// Input types for mutations
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>;

不应添加品牌类型的情况:

  • 不会跨越服务边界的简单字符串(URL、文件路径)
  • 基本配置值

有关转换和高级模式,请参阅 references/schema-patterns.md。

使用 Effect.fn 的函数模式

始终对服务方法使用 Effect.fn。它会使用恰当的跨度名称自动进行追踪:

// CORRECT - Effect.fn with descriptive name
const findById = Effect.fn("UserService.findById")(function* (id: UserId) {
  yield* Effect.annotateCurrentSpan("userId", id);
  const user = yield* repo.findById(id);
  return user;
});

// CORRECT - Effect.fn with multiple parameters
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);
  // ...
});

Layer 组合

在服务中声明依赖项,不要在使用位置声明:

// CORRECT - dependencies in service definition
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;
    // ...
  }),
}) {}

// At app root - simple merge
const AppLive = Layer.mergeAll(
  OrderService.Default,
  // Infrastructure layers (intentionally not in dependencies)
  DatabaseLive,
  RedisLive,
);

有关测试层和依赖配置的层,请参阅 references/layer-patterns.md。

Option 处理

绝不要使用 Option.getOrThrow。始终显式处理两种情况:

// CORRECT - explicit handling
yield *
  Option.match(maybeUser, {
    onNone: () => Effect.fail(new UserNotFoundError({ userId, message: "Not found" })),
    onSome: (user) => Effect.succeed(user),
  });

// CORRECT - with getOrElse for defaults
const name = Option.getOrElse(maybeName, () => "Anonymous");

// CORRECT - Option.map for transformations
const upperName = Option.map(maybeName, (n) => n.toUpperCase());

Effect Atom(前端状态)

Effect Atom 为 React 提供与 Effect 集成的响应式状态管理。

基本 Atom
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),
);
React 集成
import { useAtomValue, useAtomSet, useAtom, useAtomMount } 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>
}

// Mount side-effect atoms without reading value
function App() {
    useAtomMount(keyboardShortcutsAtom)
    return <>{children}</>
}
使用 Result.builder 处理结果

渲染带有效应的 Atom 结果时,使用 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()
}
带副作用的 Atom
const scrollYAtom = Atom.make((get) => {
  const onScroll = () => get.setSelf(window.scrollY);

  window.addEventListener("scroll", onScroll);
  get.addFinalizer(() => window.removeEventListener("scroll", onScroll)); // REQUIRED

  return window.scrollY;
}).pipe(Atom.keepAlive);

有关包含 families、localStorage 和反模式的完整模式,请参阅 references/effect-atom-patterns.md。

RPC 和 Cluster 模式

有关 RPC 契约和 Cluster 工作流,请参阅:

  • references/rpc-cluster-patterns.md - RpcGroup、Workflow.make、Activity 模式

反模式(禁止)

以下模式绝不可接受:

// FORBIDDEN - runSync/runPromise inside services
const result = Effect.runSync(someEffect); // Never do this

// FORBIDDEN - throw inside Effect.gen
yield *
  Effect.gen(function* () {
    if (bad) throw new Error("No!"); // Use Effect.fail instead
  });

// 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>

有关完整列表及其原因,请参阅 references/anti-patterns.md。

可观测性

// Structured logging
yield * Effect.log("Processing order", { orderId, userId, amount });

// Metrics
const orderCounter = Metric.counter("orders_processed");
yield * Metric.increment(orderCounter);

// Config with validation
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.Class
  • layer-patterns.md - 依赖组合、测试层
  • rpc-cluster-patterns.md - RpcGroup、Workflow、Activity 模式
  • effect-atom-patterns.md - Atom、families、React hooks、Result 处理
  • anti-patterns.md - 禁止模式的完整列表
  • observability-patterns.md - 日志记录、指标、配置模式

评论

支持 Markdown