mcpField
E
effect-ts

effect-ts

github.com/joelhooks开发441.1k 次安装23 周前更新
代码仓库

关于 effect-ts

面向使用 Effect v4 的 TypeScript 项目的开发技能,供编写、审查和重构 Effect 代码的开发者参考。它强调先查阅官方源码,并提供服务、Schema、错误处理、分层组合与测试模式。

主要功能

  1. 01优先检索 Effect 官方源码
  2. 02Effect.gen 与 Effect.fn 组合模式
  3. 03ServiceMap.Service 服务定义
  4. 04Schema 类与品牌类型建模
  5. 05Layer 组合及 Effect 测试模式

适用场景

  1. 01在 Effect 项目中实现新功能
  2. 02核对 v4 API 与当前源码
  3. 03构建服务层和依赖组合
  4. 04编写 Effect 测试与错误恢复逻辑

安装

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

$npx skills add https://github.com/joelhooks/effectts-skills --skill effect-ts

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

包含的文件

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

references/
cli.md
config.md
data-modeling.md
error-handling.md
http-clients.md
processes.md
schema-decisions.md
services-and-layers.md
setup.md
testing.md

详细介绍

Effect-TS(v4)

来自 effect-solutions 和 Effect 源码的模式。本文介绍最新的 v4 API。

源码优先规则

在使用 Effect 的任何仓库中(包/依赖文件里包含 effect 或 @effect/*),编写、审查或重构 Effect 代码之前,都要先查阅官方 Effect 源码。不要只依赖过时的记忆、博客文章或高层文档。

  • 如果有 effect_source 工具,请用它执行 status、hydrate 和 search,而不要手动编写 shell 命令。
  • 首先检查仓库本地是否有浅克隆源码镜像:.agent-sources/effect/。
  • 如果没有,请先创建镜像,再进行 Effect 相关工作: mkdir -p .agent-sources && git clone --depth 1 --filter=blob:none https://github.com/effect-ts/effect.git .agent-sources/effect
  • 不要将该镜像纳入产品提交。如有需要,将 .agent-sources/ 添加到 .git/info/exclude,而不是项目的 .gitignore,除非 Joel 明确要求提交。
  • 在将某种模式称为 Effect 最佳实践之前,先搜索镜像中的当前模式和 API,尤其是 packages/effect/src/ 及各包的测试/示例。

本地源码参考

  • 仓库本地 Effect 源码镜像(当前工作的权威来源):.agent-sources/effect/
  • effect-solutions(最佳实践、文档、示例):~/Code/kitlangton/effect-solutions/
  • 全局 Effect monorepo 备用副本:~/Code/effect-ts/effect/
  • 搜索实现源码:grep -r "pattern" .agent-sources/effect/packages/effect/src/

Effect.gen 和 Effect.fn

Effect.gen 提供顺序清晰、易读的组合方式(类似 Effect 的 async/await):

import { Effect } from "effect"

const program = Effect.gen(function* () {
  const data = yield* fetchData
  yield* Effect.logInfo(`Processing: ${data}`)
  return yield* processData(data)
})

Effect.fn 会添加调用点追踪和命名 span。所有服务方法都应使用它:

const processUser = Effect.fn("processUser")(function* (userId: string) {
  yield* Effect.logInfo(`Processing user ${userId}`)
  const user = yield* getUser(userId)
  return yield* processData(user)
})

// Second argument for cross-cutting concerns (retry, timeout)
const fetchWithRetry = Effect.fn("fetchWithRetry")(
  function* (url: string) {
    const data = yield* fetchData(url)
    return yield* processData(data)
  },
  flow(
    Effect.retry(Schedule.recurs(3)),
    Effect.timeout("5 seconds")
  )
)

ServiceMap.Service

将服务定义为带有唯一标记和类型化接口的类:

import { Effect, ServiceMap } from "effect"

class Database extends ServiceMap.Service<
  Database,
  {
    readonly query: (sql: string) => Effect.Effect<unknown[]>
    readonly execute: (sql: string) => Effect.Effect<void>
  }
>()("@app/Database") {}

使用 Layer.effect 或 Layer.sync 实现服务,并在所有方法中使用 Effect.fn:

import { Effect, Layer } from "effect"

class Users extends ServiceMap.Service<
  Users,
  {
    readonly findById: (id: UserId) => Effect.Effect<User, UserNotFoundError>
    readonly all: () => Effect.Effect<readonly User[]>
  }
>()("@app/Users") {
  static readonly layer = Layer.effect(
    Users,
    Effect.gen(function* () {
      const http = yield* HttpClient.HttpClient
      const findById = Effect.fn("Users.findById")(function* (id: UserId) {
        const response = yield* http.get(`/users/${id}`)
        return yield* HttpClientResponse.schemaBodyJson(User)(response)
      })
      const all = Effect.fn("Users.all")(function* () {
        const response = yield* http.get("/users")
        return yield* HttpClientResponse.schemaBodyJson(Schema.Array(User))(response)
      })
      return { findById, all }
    })
  )
}

规则:

  • 标记标识符必须唯一。采用 @app/ServiceName 模式
  • 服务方法的 R 应为 never(依赖通过 Layer 提供,而非方法签名)
  • 使用 readonly 属性

有关服务驱动开发、测试层、层记忆化和完整组合模式,请参阅 references/services-and-layers.md。

Schema.Class 和品牌类型

使用 Schema.Class 定义领域记录。为所有实体 ID 和领域原语添加品牌:

import { Schema } from "effect"

const UserId = Schema.String.pipe(Schema.brand("UserId"))
type UserId = typeof UserId.Type

const Email = Schema.String.pipe(Schema.brand("Email"))
type Email = typeof Email.Type

class User extends Schema.Class("User")({
  id: UserId,
  name: Schema.String,
  email: Email,
  createdAt: Schema.Date,
}) {
  get displayName() { return `${this.name} (${this.email})` }
}

// Construct with makeUnsafe for brands
const userId = UserId.makeUnsafe("user-123")

使用 Schema.TaggedClass + Schema.Union 表示变体(OR 类型):

import { Match, Schema } from "effect"

class Success extends Schema.TaggedClass("Success")("Success", {
  value: Schema.Number,
}) {}
class Failure extends Schema.TaggedClass("Failure")("Failure", {
  error: Schema.String,
}) {}
const Result = Schema.Union([Success, Failure])
type Result = typeof Result.Type

// Exhaustive pattern matching
const render = (r: Result) => Match.valueTags(r, {
  Success: ({ value }) => `Got: ${value}`,
  Failure: ({ error }) => `Error: ${error}`,
})

有关 JSON 编码、Schema.Literals、验证和完整模式,请参阅 references/data-modeling.md。

Schema.TaggedErrorClass

使用 Schema.TaggedErrorClass 定义领域错误。它们可直接 yield(无需 Effect.fail):

import { Schema } from "effect"

class UserNotFoundError extends Schema.TaggedErrorClass("UserNotFoundError")(
  "UserNotFoundError",
  { userId: UserId, message: Schema.String }
) {}

// Yieldable: yield directly in generators
const getUser = Effect.fn("getUser")(function* (id: UserId) {
  const user = yield* findUser(id)
  if (!user) yield* new UserNotFoundError({ userId: id, message: "Not found" })
  return user
})

使用 catchTag / catchTags 进行恢复:

// Single tag
const recovered = program.pipe(
  Effect.catchTag("UserNotFoundError", (e) =>
    Effect.succeed(`User ${e.userId} missing`)
  )
)

// Multiple tags
const recovered2 = program.pipe(
  Effect.catchTags({
    UserNotFoundError: (e) => Effect.succeed("not found"),
    ValidationError: (e) => Effect.succeed("invalid"),
  })
)

有关缺陷、Schema.Defect 和恢复模式,请参阅 references/error-handling.md。

Layer 组合

使用 Layer.provideMerge(增量式、扁平类型)和 Layer.merge(并行)组合层:

import { Effect, Layer } from "effect"

// Compose layers for the app
const appLayer = UserService.layer.pipe(
  Layer.provideMerge(DatabaseLayer),
  Layer.provideMerge(LoggerLayer),
  Layer.provideMerge(ConfigLayer),
)

// Provide once at the entry point
const main = program.pipe(Effect.provide(appLayer))
Effect.runPromise(main)

关键规则:

  • 将带参数的层存储在常量中(按引用身份进行层记忆化)
  • 在应用程序入口处一次性提供,不要在代码中到处提供
  • 同步实现使用 Layer.sync,效果式实现使用 Layer.effect

测试快速入门

import { describe, expect, it } from "@effect/vitest"
import { Effect, Layer } from "effect"

it.effect("queries database", () =>
  Effect.gen(function* () {
    const db = yield* Database
    const results = yield* db.query("SELECT *")
    expect(results.length).toBe(2)
  }).pipe(Effect.provide(Database.testLayer))
)
  • 效果式测试使用 it.effect(提供带有 TestClock 的 TestContext)
  • 实时时间/真实时钟使用 it.live
  • 每项测试都提供全新的层,以防止状态泄漏
  • 仅在需要在整个测试套件中共享昂贵资源时使用 it.layer

完整示例和高级模式请参阅 references/testing.md。

使用 Pipe 进行插桩

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"),
)

反模式

不要这样做应该这样做
console.log(...)使用包含结构化数据的 Effect.log(...)
process.env.KEY使用 Config.string("KEY") 或 Config.redacted("KEY")
在 Effect.gen 中使用 throw new Error()使用 yield* new TaggedError({...}) 或 Effect.fail(...)
在服务中使用 Effect.runSync(...)保持所有操作都具有副作用
使用 Effect.catchAll(() => ...),导致类型信息丢失使用 Effect.catchTag / Effect.catchTags
在领域类型中使用 null / undefined使用 Option<T> 和 Option.match
使用 Option.getOrThrow(...)使用 Option.match({ onNone, onSome }) 或 Option.getOrElse
使用 Effect.Service(v3)使用 ServiceMap.Service(v4)
使用 Schema.TaggedError<T>()(v3)使用 Schema.TaggedErrorClass("Tag")("Tag", {...})(v4)
分散调用 Effect.provide在应用入口处统一提供
在行内调用带参数的层构造函数将层存储在常量中(以便记忆化)

参考文件

根据需要加载以下文件,了解更深入的模式:

  • 服务与层:ServiceMap.Service、由服务驱动的开发、测试层、层记忆化、provide 与 provideMerge
  • 数据建模:Schema.Class、品牌类型、变体、Match.valueTags、JSON 编码
  • Schema 决策:Schema.Class、Struct 与 TaggedClass 的决策流程图、迁移模式
  • 错误处理:Schema.TaggedErrorClass、catch/catchTag/catchTags、缺陷、Schema.Defect、TypeId/refail 模式
  • 测试:@effect/vitest 设置、it.effect/it.live/it.layer、TestClock、Effect.flip、FiberRef 隔离、完整示例
  • HTTP 客户端:HttpClient、请求构建、响应解码、中间件、重试、类型化 API 服务
  • CLI:Command.make、Arguments、Flags、子命令、任务管理器完整示例
  • 配置:Config 模块、Schema 验证、ConfigProvider、Redacted、配置层
  • 进程与作用域:Fork 类型、Scope.extend、用于子进程的 Command、可终止的后台任务
  • 设置:tsconfig、Effect Language Service、项目结构、模块设置

评论

支持 Markdown