--- name: effect-ts description: "Write idiomatic Effect v4 TypeScript following official best practices from effect-solutions and the Effect source. Use when writing, reviewing, or refactoring Effect code: services (ServiceMap.Service), layers and dependency injection, error handling (Schema.TaggedErrorClass), data modeling (Schema.Class, branded types, variants), testing (@effect/vitest), HTTP clients (effect/unstable/http), CLI tools (effect/unstable/cli), config, observability, and project setup. Triggers on: 'Effect', 'effect-ts', '@effect/', 'Schema', 'ServiceMap', 'Layer', 'Effect.gen', 'Effect.fn', 'TaggedError', 'branded types', or any Effect-TS related code." --- # Effect-TS (v4) Patterns from [effect-solutions](https://effect.solutions) and the [Effect source](https://github.com/effect-ts/effect). This covers the latest v4 APIs. ## Source-First Rule When working in any repo that uses Effect (`effect` or `@effect/*` in package/dependency files), reference the official Effect source before writing, reviewing, or refactoring Effect code. Do not rely on stale memory, blog posts, or high-level docs alone. - If the `effect_source` tool is available, use it for `status`, `hydrate`, and `search` instead of hand-rolled shell commands. - First check for a repo-local shallow source mirror at `.agent-sources/effect/`. - If it is missing, create it before doing Effect work: `mkdir -p .agent-sources && git clone --depth 1 --filter=blob:none https://github.com/effect-ts/effect.git .agent-sources/effect` - Keep the mirror out of product commits. If needed, add `.agent-sources/` to `.git/info/exclude`, not the project `.gitignore`, unless Joel explicitly wants it committed. - Search the mirror for current patterns and APIs, especially under `packages/effect/src/` and package tests/examples, before calling something an Effect best practice. ## Local Source References - **repo-local Effect source mirror** (canonical for current work): `.agent-sources/effect/` - **effect-solutions** (best practices, docs, examples): `~/Code/kitlangton/effect-solutions/` - **fallback global Effect monorepo**: `~/Code/effect-ts/effect/` - Search source for implementations: `grep -r "pattern" .agent-sources/effect/packages/effect/src/` ## Effect.gen and Effect.fn `Effect.gen` provides sequential, readable composition (like async/await for Effect): ```typescript import { Effect } from "effect" const program = Effect.gen(function* () { const data = yield* fetchData yield* Effect.logInfo(`Processing: ${data}`) return yield* processData(data) }) ``` `Effect.fn` adds call-site tracing and named spans. Use for all service methods: ```typescript 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 Define services as classes with a unique tag and typed interface: ```typescript import { Effect, ServiceMap } from "effect" class Database extends ServiceMap.Service< Database, { readonly query: (sql: string) => Effect.Effect readonly execute: (sql: string) => Effect.Effect } >()("@app/Database") {} ``` Implement with `Layer.effect` or `Layer.sync`, using `Effect.fn` for all methods: ```typescript import { Effect, Layer } from "effect" class Users extends ServiceMap.Service< Users, { readonly findById: (id: UserId) => Effect.Effect readonly all: () => Effect.Effect } >()("@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 } }) ) } ``` **Rules:** - Tag identifiers must be unique. Use `@app/ServiceName` pattern - Service methods should have `R = never` (dependencies via Layer, not method signatures) - Use `readonly` properties See [references/services-and-layers.md](references/services-and-layers.md) for service-driven development, test layers, layer memoization, and full composition patterns. ## Schema.Class and Branded Types Use `Schema.Class` for domain records. Brand all entity IDs and domain primitives: ```typescript 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") ``` Use `Schema.TaggedClass` + `Schema.Union` for variants (OR types): ```typescript 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}`, }) ``` See [references/data-modeling.md](references/data-modeling.md) for JSON encoding, Schema.Literals, validation, and full patterns. ## Schema.TaggedErrorClass Define domain errors with `Schema.TaggedErrorClass`. They are yieldable (no `Effect.fail` needed): ```typescript 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 }) ``` Recover with `catchTag` / `catchTags`: ```typescript // 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"), }) ) ``` See [references/error-handling.md](references/error-handling.md) for defects, Schema.Defect, and recovery patterns. ## Layer Composition Compose layers with `Layer.provideMerge` (incremental, flat types) and `Layer.merge` (parallel): ```typescript 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) ``` **Key rules:** - Store parameterized layers in constants (layer memoization by reference identity) - Provide once at app entry, not scattered throughout code - Use `Layer.sync` for synchronous implementations, `Layer.effect` for effectful ones ## Testing Quick Start ```typescript 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)) ) ``` - Use `it.effect` for Effect-based tests (provides TestContext with TestClock) - Use `it.live` for real time / real clock - Provide fresh layers per test to prevent state leakage - Use `it.layer` only when sharing expensive resources across a suite See [references/testing.md](references/testing.md) for the full worked example and advanced patterns. ## Pipe for Instrumentation ```typescript 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"), ) ``` ## Anti-Patterns | Do Not | Do Instead | |--------|-----------| | `console.log(...)` | `Effect.log(...)` with structured data | | `process.env.KEY` | `Config.string("KEY")` or `Config.redacted("KEY")` | | `throw new Error()` inside `Effect.gen` | `yield* new TaggedError({...})` or `Effect.fail(...)` | | `Effect.runSync(...)` inside services | Keep everything effectful | | `Effect.catchAll(() => ...)` losing type info | `Effect.catchTag` / `Effect.catchTags` | | `null` / `undefined` in domain types | `Option` with `Option.match` | | `Option.getOrThrow(...)` | `Option.match({ onNone, onSome })` or `Option.getOrElse` | | `Effect.Service` (v3) | `ServiceMap.Service` (v4) | | `Schema.TaggedError()` (v3) | `Schema.TaggedErrorClass("Tag")("Tag", {...})` (v4) | | Scatter `Effect.provide` calls | Provide once at app entry | | Call parameterized layer constructors inline | Store layers in constants (memoization) | ## Reference Files Load these as needed for deeper patterns: - **[Services & Layers](references/services-and-layers.md)**: ServiceMap.Service, service-driven development, test layers, layer memoization, provide vs provideMerge - **[Data Modeling](references/data-modeling.md)**: Schema.Class, branded types, variants, Match.valueTags, JSON encoding - **[Schema Decisions](references/schema-decisions.md)**: Schema.Class vs Struct vs TaggedClass decision flowchart, migration patterns - **[Error Handling](references/error-handling.md)**: Schema.TaggedErrorClass, catch/catchTag/catchTags, defects, Schema.Defect, TypeId/refail patterns - **[Testing](references/testing.md)**: @effect/vitest setup, it.effect/it.live/it.layer, TestClock, Effect.flip, FiberRef isolation, worked example - **[HTTP Clients](references/http-clients.md)**: HttpClient, request building, response decoding, middleware, retries, typed API service - **[CLI](references/cli.md)**: Command.make, Arguments, Flags, subcommands, worked task manager example - **[Config](references/config.md)**: Config module, schema validation, ConfigProvider, Redacted, config layers - **[Processes & Scopes](references/processes.md)**: Fork types, Scope.extend, Command for child processes, killable background tasks - **[Setup](references/setup.md)**: tsconfig, Effect Language Service, project structure, module settings