$npx -y skills add joelhooks/effectts-skills --skill effect-tsWrite 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), d
| 1 | # Effect-TS (v4) |
| 2 | |
| 3 | Patterns from [effect-solutions](https://effect.solutions) and the [Effect source](https://github.com/effect-ts/effect). This covers the latest v4 APIs. |
| 4 | |
| 5 | ## Source-First Rule |
| 6 | |
| 7 | 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. |
| 8 | |
| 9 | - If the `effect_source` tool is available, use it for `status`, `hydrate`, and `search` instead of hand-rolled shell commands. |
| 10 | - First check for a repo-local shallow source mirror at `.agent-sources/effect/`. |
| 11 | - If it is missing, create it before doing Effect work: |
| 12 | `mkdir -p .agent-sources && git clone --depth 1 --filter=blob:none https://github.com/effect-ts/effect.git .agent-sources/effect` |
| 13 | - 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. |
| 14 | - Search the mirror for current patterns and APIs, especially under `packages/effect/src/` and package tests/examples, before calling something an Effect best practice. |
| 15 | |
| 16 | ## Local Source References |
| 17 | |
| 18 | - **repo-local Effect source mirror** (canonical for current work): `.agent-sources/effect/` |
| 19 | - **effect-solutions** (best practices, docs, examples): `~/Code/kitlangton/effect-solutions/` |
| 20 | - **fallback global Effect monorepo**: `~/Code/effect-ts/effect/` |
| 21 | - Search source for implementations: `grep -r "pattern" .agent-sources/effect/packages/effect/src/` |
| 22 | |
| 23 | ## Effect.gen and Effect.fn |
| 24 | |
| 25 | `Effect.gen` provides sequential, readable composition (like async/await for Effect): |
| 26 | |
| 27 | ```typescript |
| 28 | import { Effect } from "effect" |
| 29 | |
| 30 | const program = Effect.gen(function* () { |
| 31 | const data = yield* fetchData |
| 32 | yield* Effect.logInfo(`Processing: ${data}`) |
| 33 | return yield* processData(data) |
| 34 | }) |
| 35 | ``` |
| 36 | |
| 37 | `Effect.fn` adds call-site tracing and named spans. Use for all service methods: |
| 38 | |
| 39 | ```typescript |
| 40 | const processUser = Effect.fn("processUser")(function* (userId: string) { |
| 41 | yield* Effect.logInfo(`Processing user ${userId}`) |
| 42 | const user = yield* getUser(userId) |
| 43 | return yield* processData(user) |
| 44 | }) |
| 45 | |
| 46 | // Second argument for cross-cutting concerns (retry, timeout) |
| 47 | const fetchWithRetry = Effect.fn("fetchWithRetry")( |
| 48 | function* (url: string) { |
| 49 | const data = yield* fetchData(url) |
| 50 | return yield* processData(data) |
| 51 | }, |
| 52 | flow( |
| 53 | Effect.retry(Schedule.recurs(3)), |
| 54 | Effect.timeout("5 seconds") |
| 55 | ) |
| 56 | ) |
| 57 | ``` |
| 58 | |
| 59 | ## ServiceMap.Service |
| 60 | |
| 61 | Define services as classes with a unique tag and typed interface: |
| 62 | |
| 63 | ```typescript |
| 64 | import { Effect, ServiceMap } from "effect" |
| 65 | |
| 66 | class Database extends ServiceMap.Service< |
| 67 | Database, |
| 68 | { |
| 69 | readonly query: (sql: string) => Effect.Effect<unknown[]> |
| 70 | readonly execute: (sql: string) => Effect.Effect<void> |
| 71 | } |
| 72 | >()("@app/Database") {} |
| 73 | ``` |
| 74 | |
| 75 | Implement with `Layer.effect` or `Layer.sync`, using `Effect.fn` for all methods: |
| 76 | |
| 77 | ```typescript |
| 78 | import { Effect, Layer } from "effect" |
| 79 | |
| 80 | class Users extends ServiceMap.Service< |
| 81 | Users, |
| 82 | { |
| 83 | readonly findById: (id: UserId) => Effect.Effect<User, UserNotFoundError> |
| 84 | readonly all: () => Effect.Effect<readonly User[]> |
| 85 | } |
| 86 | >()("@app/Users") { |
| 87 | static readonly layer = Layer.effect( |
| 88 | Users, |
| 89 | Effect.gen(function* () { |
| 90 | const http = yield* HttpClient.HttpClient |
| 91 | const findById = Effect.fn("Users.findById")(function* (id: UserId) { |
| 92 | const response = yield* http.get(`/users/${id}`) |
| 93 | return yield* HttpClientResponse.schemaBodyJson(User)(response) |
| 94 | }) |
| 95 | const all = Effect.fn("Users.all")(function* () { |
| 96 | const response = yield* http.get("/users") |
| 97 | return yield* HttpClientResponse.schemaBodyJson(Schema.Array(User))(response) |
| 98 | }) |
| 99 | return { findById, all } |
| 100 | }) |
| 101 | ) |
| 102 | } |
| 103 | ``` |
| 104 | |
| 105 | **Rules:** |
| 106 | - Tag identifiers must be unique. Use `@app/ServiceName` pattern |
| 107 | - Service methods should have `R = never` (dependencies via Layer, not method signatures) |
| 108 | - Use `readonly` properties |
| 109 | |
| 110 | See [references/services-and-layers.md](references/services-and-layers.md) for service-driven development, test layers, layer memoization, and full composition patterns. |
| 111 | |
| 112 | ## Schema.Class and Branded Types |
| 113 | |
| 114 | Use `Schema.Class` for domain records. Brand all entity IDs and domain primitives: |
| 115 | |
| 116 | ```typescript |
| 117 | impo |