| 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 and the Effect source. 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):
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:
const processUser = Effect.fn("processUser")(function* (userId: string) {
yield* Effect.logInfo(`Processing user ${userId}`)
const user = yield* getUser(userId)
return yield* processData(user)
})
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:
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") {}
Implement with Layer.effect or Layer.sync, using Effect.fn for all methods:
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 }
})
)
}
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 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:
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})` }
}
const userId = UserId.makeUnsafe("user-123")
Use Schema.TaggedClass + Schema.Union for variants (OR types):
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
const render = (r: Result) => Match.valueTags(r, {
Success: ({ value }) => `Got: ${value}`,
Failure: ({ error }) => `Error: ${error}`,
})
See 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):
import { Schema } from "effect"
class UserNotFoundError extends Schema.TaggedErrorClass("UserNotFoundError")(
"UserNotFoundError",
{ userId: UserId, message: Schema.String }
) {}
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:
const recovered = program.pipe(
Effect.catchTag("UserNotFoundError", (e) =>
Effect.succeed(`User ${e.userId} missing`)
)
)
const recovered2 = program.pipe(
Effect.catchTags({
UserNotFoundError: (e) => Effect.succeed("not found"),
ValidationError: (e) => Effect.succeed("invalid"),
})
)
See 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):
import { Effect, Layer } from "effect"
const appLayer = UserService.layer.pipe(
Layer.provideMerge(DatabaseLayer),
Layer.provideMerge(LoggerLayer),
Layer.provideMerge(ConfigLayer),
)
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
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 for the full worked example and advanced patterns.
Pipe for Instrumentation
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<T> with Option.match |
Option.getOrThrow(...) | Option.match({ onNone, onSome }) or Option.getOrElse |
Effect.Service (v3) | ServiceMap.Service (v4) |
Schema.TaggedError<T>() (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: ServiceMap.Service, service-driven development, test layers, layer memoization, provide vs provideMerge
- Data Modeling: Schema.Class, branded types, variants, Match.valueTags, JSON encoding
- Schema Decisions: Schema.Class vs Struct vs TaggedClass decision flowchart, migration patterns
- Error Handling: Schema.TaggedErrorClass, catch/catchTag/catchTags, defects, Schema.Defect, TypeId/refail patterns
- Testing: @effect/vitest setup, it.effect/it.live/it.layer, TestClock, Effect.flip, FiberRef isolation, worked example
- HTTP Clients: HttpClient, request building, response decoding, middleware, retries, typed API service
- CLI: Command.make, Arguments, Flags, subcommands, worked task manager example
- Config: Config module, schema validation, ConfigProvider, Redacted, config layers
- Processes & Scopes: Fork types, Scope.extend, Command for child processes, killable background tasks
- Setup: tsconfig, Effect Language Service, project structure, module settings