Effect TypeScript
Use Effect for application I/O so failures, dependencies, and resource lifetimes are explicit. Adapt a Promise-based driver or SDK once at its boundary; keep internal operations in Effect. Run Effects at the caller or runtime boundary that requires a Promise or starts the program. Pure transformations remain plain TypeScript.
Follow codebase-design for the shared readability rules and examples. Keep related queries, bindings, decoding, and private helpers with their adapter; extract only when it hides complexity or enables useful reuse. Operations own completion, errors, and cleanup, or explicitly hand that responsibility to their caller.
This skill adapts Effect guidance to darkmatter conventions: use Bun commands instead of pnpm for darkmatter projects, and prefer Alchemy for deployable infrastructure.
Bootstrap
If
is not on in your
, install it:
bun add -g effect-solutions@latest
. Prefer the repository's pinned Effect source or installed package for API details; use a version-matching checkout when neither is available.
For new Darkmatter TypeScript repos, align dependencies with the
root catalog. Use Effect 4,
,
,
, and
; prefer the
Effect agent APIs (
) before inventing agent loop contracts.
Guidelines
- Refer to the best practices recommended by the CLI when writing effectful code.
- If you need more specific information, there's a local checkout of the effect source in the reference directory.
When to use
- Meaningful I/O is involved and the work benefits from explicit failure, dependency, resource, retry, validation, or testing boundaries.
- The code already uses Effect and you are adding or reviewing Effect code.
- You are deciding whether a TypeScript/Bun feature should use Effect.
- The task involves external APIs, databases, queues, workers, CLIs, schedules, retries, config, secrets, structured logging, runtime validation, resource cleanup, or concurrent workflows.
- You need typed domain errors rather than unstructured thrown exceptions.
- You need swappable live/test implementations through services and Layers.
- You are deploying TypeScript infrastructure or workers and need Alchemy-aware conventions.
When NOT to use
- A small one-off script can be obvious plain TypeScript: read one file, transform pure data, write one file, no retries, no injected dependencies, no long-lived resources. As requirements grow, adopt Effect where I/O needs it and extract coherent responsibilities. A line count alone does not justify scattering the implementation; retain configured limits with documented, targeted exceptions where needed.
- Pure functions, simple data mappers, UI-local state, or tiny glue code do not need Effect wrappers.
- A project has no Effect dependency and the feature does not benefit from typed errors, Layers, resource safety, retries, or observability.
- The team only needs a tactical fix in plain async code. Do not introduce Effect as a drive-by refactor.
- You cannot explain the service/layer/error/testing shape. Stop and design that first instead of sprinkling calls everywhere.
Darkmatter Conventions
- Use Bun commands: , , , .
- Translate upstream examples mechanically. Example: becomes when the project uses Bun test, or
bun run test file.test.ts
when test is a package script.
- Prefer Bun runtime packages where relevant, such as and for Bun entrypoints.
- Prefer Alchemy for deployable infrastructure. Put infra in , create resources with Alchemy, bind them to workers/services, and build up an . Refer to the alchemy skill if writing alchemy code or adding new infra code.
- Prefer Postgres for application data. Use Kysely + for query-heavy TypeScript adapters, or Effect SQL Postgres when persistence is exposed as Effect services and Layers. D1/sqlite are exceptions for narrow edge-local state, demos, or an explicit project decision.
- Configuration comes from named config files () read through . There is no selector flag: an interactive run picks from a list (), a non-interactive run reads or fails listing the names. Env vars and flags are per-key overrides chained with ; secrets come from the sibling via . Do not design the setup of a program as a list of flags or env vars. See ADR-0014.
Typed RPC with effect-orpc
Use
when a service needs typed RPC procedures, a generated or
shared oRPC client contract, Effect service injection, and typed Effect errors.
Use raw Effect
or Hono when the route is simple HTTP, a webhook,
static content, streaming/SSE, or a framework adapter where an RPC contract
would add ceremony without a client-facing type contract.
When adding
, pin the Effect 4 build in the catalog:
json
{
"workspaces": {
"catalog": {
"effect-orpc": "1.0.0-effect-v4.8",
"@orpc/server": "^1.13.0",
"@orpc/client": "^1.13.0",
"@orpc/contract": "^1.13.0",
"@orpc/shared": "^1.13.0"
}
}
}
Do not install
; npm
is the Effect 3 line
(
). The
dist-tag currently resolves to
, whose peers require Effect 4
(
) and the oRPC packages above.
Prefer these upstream shapes:
- Build procedures from or .
Use a caller-owned when a scoped layer should be acquired
once and disposed during application shutdown, such as a Postgres pool,
telemetry SDK, or shared cache.
- Write handlers and Effect middleware with
.effect(function* (...) { ... })
and generator .use(function* ({ next }) { return yield* next(...) })
so
service requirements, spans, cancellation, and errors stay in Effect.
- Model HTTP errors with and pass those classes to
/ ; do not duplicate the same error contract
as unrelated thrown exceptions and response schemas.
- Use
implementEffect(contract, layerOrRuntime)
when a repo is contract-first
and already owns an oRPC contract. Contract leaves keep the declared input,
output, and error surface while adding handlers.
- Import only when Node is needed to
preserve FiberRefs across native oRPC middleware or framework boundaries.
Use
withFiberContext(() => next())
when request-local FiberRef state is
created outside the oRPC pipeline and must be visible inside handlers.
Package layout reference
See effect-package-map.md for domain ownership, public package entries, adapters, workflows, tests, and runtime boundaries. It describes responsibilities rather than prescribing a file for each function or Effect export.
Upstream Reference
Use
or
CLI when you need current Effect source or examples instead of relying on memory:
reference/effect/AGENTS.md
— upstream repository rules, including pnpm validation commands, generated barrels, changesets, and conventions.
reference/effect/packages/effect/
— core library source and tests.
reference/effect/packages/platform-bun/
and reference/effect/packages/platform-node/
— runtime/platform examples.
reference/effect/packages/vitest/
— Effect-aware Vitest helpers.
If the submodule is missing in a fresh checkout, initialize it before using local references:
bash
git submodule update --init skills/effect-typescript/reference/effect
For darkmatter application work, treat the submodule as read-only and translate upstream pnpm commands to the repo's package manager. For direct upstream Effect contributions, follow
reference/effect/AGENTS.md
exactly; do not apply darkmatter Bun defaults inside the upstream repo.
JSON Encoding & Decoding
Use
to parse JSON strings and validate them with your schema in one step. This combines
+ schema decoding in one step, and
+ schema encoding for the reverse:
typescript
import { Effect, Schema } from "effect"
const Row = Schema.Literals(["A", "B", "C", "D", "E", "F", "G", "H"])
const Column = Schema.Literals(["1", "2", "3", "4", "5", "6", "7", "8"])
class Position extends Schema.Class<Position>("Position")({
row: Row,
column: Column,
}) {}
class Move extends Schema.Class<Move>("Move")({
from: Position,
to: Position,
}) {}
// fromJsonString combines JSON.parse + schema decoding
// MoveFromJson is a schema that takes a JSON string and returns a Move
const MoveFromJson = Schema.fromJsonString(Move)
const program = Effect.gen(function* () {
// Parse and validate JSON string in one step
// Use MoveFromJson (not Move) to decode from JSON string
const jsonString = '{"from":{"row":"A","column":"1"},"to":{"row":"B","column":"2"}}'
const move = yield* Schema.decodeUnknownEffect(MoveFromJson)(jsonString)
yield* Effect.log("Decoded move", move)
// Encode to JSON string in one step (typed as string)
// Use MoveFromJson (not Move) to encode to JSON string
const json = yield* Schema.encodeEffect(MoveFromJson)(move)
return json
})