Skip to main content

effect-best-practices

Enforces Effect-TS patterns for services, errors, layers, and atoms. Use when writing code with Effect.Service, Schema.TaggedError, Layer composition, or effect-atom React components.

跳到安装

来源信息

仓库
forcedotcom/apex-language-support
最近来源活动
2026年6月23日 22:16
检测到的 SKILL.md 语言
英语
星标
11
分支
3

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
10 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
effect-best-practices
description
Enforces Effect-TS patterns for services, errors, layers, and atoms. Use when writing code with Effect.Service, Schema.TaggedError, Layer composition, or effect-atom React components.
version
1.2.0
# Effect-TS Best Practices This skill enforces opinionated, consistent patterns for Effect-TS codebases. These patterns optimize for type safety, testability, observability, and maintainability. For diff/plan review against these patterns, invoke the `effect-advocate` subagent (`.claude/agents/effect-advocate.md`). ## Effect LS diagnostics (agent usage) Cursor's `read_lints` does not surface Effect Language Server diagnostics. Use the CLI: ```bash npx effect-language-service diagnostics --file <path> # or whole project: npx effect-language-service diagnostics --project tsconfig.json ``` - Run when editing Effect code; fix reported issues (e.g. `unnecessaryFailYieldableError` → yield error directly) - `effect-language-service quickfixes` shows proposed code changes ## Quick Reference: Critical Rules | Category | DO | DON'T | |----------|-----|-------| | Services | `Effect.Service` with `accessors: true` | `Context.Tag` for business logic | | Dependencies | `dependencies: [Dep.Default]` in service | Manual `Layer.provide` at usage sites | | Errors | `Schema.TaggedError` with `message` field | Plain classes or generic Error | | Error Specificity | `UserNotFoundError`, `SessionExpiredError` | Generic `NotFoundError`, `BadRequestError` | | Error Handling | `catchTag`/`catchTags`; catch only when needed | `catchAll`; swallowing; catching "just in case" | | IDs | `Schema.UUID.pipe(Schema.brand("@App/EntityId"))` | Plain `string` for entity IDs | | Functions | `Effect.fn` over `Effect.gen`; `.gen` only for shared pipes | Anonymous generators; `.gen` for business logic | | Params vs deps | Params = runtime data; dependencies = yield from context | Passing Ref/PubSub/service as params | | Naming | `FooCommand` for commands, domain names for helpers | `FooEffect` suffix (redundant; TS/Effect.fn already convey type) | | Logging | `Effect.log` with structured data | `console.log` | | Config | `Config.*` with validation | `process.env` directly (except build-time vars like `ESBUILD_*`) | | Options | `Option.match` with both cases | `Option.getOrThrow` | | Nullability | `Option<T>` in domain types | `null`/`undefined` | | Atoms | `Atom.make` outside components | Creating atoms inside render | | Atom State | `Atom.keepAlive` for global state | Forgetting keepAlive for persistent state | | Atom Updates | `useAtomSet` in React components | `Atom.update` imperatively from React | | Atom Cleanup | `get.addFinalizer()` for side effects | Missing cleanup for event listeners | | Atom Results | `Result.builder` with `onErrorTag` | Ignoring loading/error states | ## Service Definition Pattern **Always use `Effect.Service`** for business logic services. This provides automatic accessors, built-in `Default` layer, and proper dependency declaration. ```typescript 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) ``` **When `Context.Tag` is acceptable:** - Infrastructure with runtime injection (Cloudflare KV, worker bindings) - Factory patterns where resources are provided externally ### Params vs Dependencies - **Params** = runtime data per call (IDs, user input, per-invocation config) - **Dependencies** = shared infrastructure (Ref, PubSub, SubscriptionRef, services) — provide via layer, **yield inside** the effect - Build Ref/PubSub/etc in the layer (e.g. `buildAllServicesLayer`); consumers yield them, don't receive as params ```typescript // WRONG - passing shared infra as params const createStatusBar = (pubsub: PubSub.PubSub<void>, stateRef: SubscriptionRef.SubscriptionRef<State>) => Effect.gen(...) // Caller must create and pass; wiring scattered at call sites // CORRECT - yield inside, build in layer const PubSubTag = Context.GenericTag<PubSub.PubSub<void>>("PubSub") const createStatusBar = Effect.gen(function* () { const pubsub = yield* PubSubTag const stateRef = yield* StateRefTag // ... }) // Layer: Layer.effect(PubSubTag, PubSub.sliding<void>(1)) ``` See `references/service-patterns.md` for detailed patterns. ## Error Definition Pattern **Always use `Schema.TaggedError`** for errors. This makes them serializable (required for RPC) and provides consistent structure. ```typescript 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 }), ) {} ``` **Error handling - use `catchTag`/`catchTags`:** ```typescript // 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 })), }), ) ``` ### When to Catch (and When Not To) **Most errors surface to the user** (message/toast at runtime). Only catch when: - **Genuinely ignore** – accept failure and continue (e.g. optional pre-create) - **Better message** – default vague; map to clearer domain error Catch sparingly. No `catchAll` or "swallow to be safe." Use `catchTag`/`catchTags`; log or fail with improved error. ### Prefer Explicit Over Generic Errors **Every distinct failure reason deserves its own error type.** Don't collapse multiple failure modes into generic HTTP errors. ```typescript // 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! ``` ```typescript // 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." ``` See `references/error-patterns.md` for error remapping and retry patterns. ## Schema & Branded Types Pattern **Brand all entity IDs** for type safety across service boundaries: ```typescript 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> ``` **When NOT to brand:** - Simple strings that don't cross service boundaries (URLs, file paths) - Primitive config values See `references/schema-patterns.md` for transforms and advanced patterns. ## Function Pattern: Prefer Effect.fn over Effect.gen **Prefer `Effect.fn`** for effectful code. Provides automatic tracing with proper span names. Span name is required. **Use `Effect.gen` only when** you need a shared effect with common `.pipe` attached so multiple consumers don't each pipe the same things — e.g. provided dependencies, common error handlers, retries. (Less common with Runtimes.) Service definition bodies are a valid use (shared wiring). ```typescript // 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) // ... } ) // WRONG - params on wrapper arrow, generator has none (closure capture) const findByIdBad = (id: UserId) => Effect.fn("UserService.findById")(function* () { yield* repo.findById(id) // id from closure }) // Naming: Don't append Effect. For commands use FooCommand; for helpers/lifecycle use domain names. // WRONG: logGetEffect, executeAnonymousDocumentEffect, activateEffect // CORRECT: logGetCommand, executeAnonymousCommand, executeAnonymous (helper), activation (lifecycle) ``` ## Layer Composition **Declare dependencies in the service**, not at usage sites: ```typescript // 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, ) ``` See `references/layer-patterns.md` for testing layers and config-dependent layers. ## Option Handling **Never use `Option.getOrThrow`**. Always handle both cases explicitly: ```typescript // 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 (Frontend State) Effect Atom provides reactive state management for React with Effect integration. ### 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
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看