Effect TypeScript
Our engineering convention forbids the usage of promises and instead uses effects. Unlike promises, effects are able to describe the errors they can throw in its declared type. They are most useful anywhere where I/O occurs: external APIs, files, databases, queues, workers, CLIs, config, secrets, clocks, subprocesses, network calls, or deployable runtime boundaries. Effect is excellent when that I/O needs typed failures, dependencies, runtime validation, retries, concurrency, resources, and testable boundaries. It is not a default replacement for simple TypeScript.
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 effect-solutions is not on in your $PATH, install it: bun add -g effect-solutions@latest. Clone the latest HEAD of effect's source to ~/.agents/repos/effect-ts/effect.
Guidelines
- Refer to the best practices recommended by the
effect-solutions 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. Keep this exception genuinely small; if the file starts accumulating schemas, clients, orchestration, retries, or reusable helpers, split it around those boundaries instead of growing a monolith.
- 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
Effect.runPromise calls everywhere.
Darkmatter Conventions
- Use Bun commands:
bun install, bun test, bun run <script>, bunx <tool>.
- Translate upstream
pnpm examples mechanically. Example: pnpm test file.test.ts becomes bun test file.test.ts 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
@effect/platform-bun and BunRuntime.runMain for Bun entrypoints.
- Prefer Alchemy for deployable infrastructure. Put infra in
alchemy.run.ts, create resources with Alchemy, bind them to workers/services, and build up an Alchemy.Stack. Refer to the alchemy skill if writing alchemy code or adding new infra code.
- Configuration comes from named config files (
config/<name>.json) read through Config. There is no selector flag: an interactive run picks from a list (Prompt.select), a non-interactive run reads APP_CONFIG or fails listing the names. Env vars and flags are per-key overrides chained with ConfigProvider.orElse; secrets come from the .sops.json sibling via effect-sops. Do not design the setup of a program as a list of flags or env vars. See ADR-0014.
Package layout reference
For the canonical Effect package shape - which directory/file owns which Effect export (domain, services, adapters, workflows, CLI/HTTP boundaries, tests, Alchemy deploy) - see the effect-package-map.md file next to this SKILL.md. Use it when scaffolding a new package or reviewing where an Effect import landed.
Upstream Reference
Use reference/effect or effect-solutions 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 it.effect 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:
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 Schema.fromJsonString to parse JSON strings and validate them with your schema in one step. This combines JSON.parse + schema decoding in one step, and JSON.stringify + schema encoding for the reverse:
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
})
1---2name: effect-typescript3description: Effect is a hard default unless the codebase explicitly states it. Use this skill when writing any Typescript code.4---56# Effect TypeScript78Our engineering convention forbids the usage of promises and instead uses effects. Unlike promises, effects are able to describe the errors they can throw in its declared type. They are most useful anywhere where I/O occurs: external APIs, files, databases, queues, workers, CLIs, config, secrets, clocks, subprocesses, network calls, or deployable runtime boundaries. Effect is excellent when that I/O needs typed failures, dependencies, runtime validation, retries, concurrency, resources, and testable boundaries. It is not a default replacement for simple TypeScript.91011This skill adapts Effect guidance to darkmatter conventions: use Bun commands instead of pnpm for darkmatter projects, and prefer Alchemy for deployable infrastructure. 1213## Bootstrap1415If `effect-solutions` is not on in your `$PATH`, install it: `bun add -g effect-solutions@latest`. Clone the latest HEAD of effect's source to `~/.agents/repos/effect-ts/effect`.1617## Guidelines1819- Refer to the best practices recommended by the `effect-solutions` CLI when writing effectful code.20- If you need more specific information, there's a local checkout of the effect source in the reference directory.2122## When to use2324- Meaningful I/O is involved and the work benefits from explicit failure, dependency, resource, retry, validation, or testing boundaries.25- The code already uses Effect and you are adding or reviewing Effect code.26- You are deciding whether a TypeScript/Bun feature should use Effect.27- The task involves external APIs, databases, queues, workers, CLIs, schedules, retries, config, secrets, structured logging, runtime validation, resource cleanup, or concurrent workflows.28- You need typed domain errors rather than unstructured thrown exceptions.29- You need swappable live/test implementations through services and Layers.30- You are deploying TypeScript infrastructure or workers and need Alchemy-aware conventions.3132## When NOT to use3334- 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. Keep this exception genuinely small; if the file starts accumulating schemas, clients, orchestration, retries, or reusable helpers, split it around those boundaries instead of growing a monolith.35- Pure functions, simple data mappers, UI-local state, or tiny glue code do not need Effect wrappers.36- A project has no Effect dependency and the feature does not benefit from typed errors, Layers, resource safety, retries, or observability.37- The team only needs a tactical fix in plain async code. Do not introduce Effect as a drive-by refactor.38- You cannot explain the service/layer/error/testing shape. Stop and design that first instead of sprinkling `Effect.runPromise` calls everywhere.3940## Darkmatter Conventions4142- Use Bun commands: `bun install`, `bun test`, `bun run <script>`, `bunx <tool>`.43- Translate upstream `pnpm` examples mechanically. Example: `pnpm test file.test.ts` becomes `bun test file.test.ts` when the project uses Bun test, or `bun run test file.test.ts` when test is a package script.44- Prefer Bun runtime packages where relevant, such as `@effect/platform-bun` and `BunRuntime.runMain` for Bun entrypoints.45- Prefer Alchemy for deployable infrastructure. Put infra in `alchemy.run.ts`, create resources with Alchemy, bind them to workers/services, and build up an `Alchemy.Stack`. Refer to the alchemy skill if writing alchemy code or adding new infra code.46- Configuration comes from named config files (`config/<name>.json`) read through `Config`. There is no selector flag: an interactive run picks from a list (`Prompt.select`), a non-interactive run reads `APP_CONFIG` or fails listing the names. Env vars and flags are per-key overrides chained with `ConfigProvider.orElse`; secrets come from the `.sops.json` sibling via `effect-sops`. Do not design the setup of a program as a list of flags or env vars. See [ADR-0014](../../docs/adr/0014-named-config-files-over-flags-and-env.md).4748## Package layout reference4950For the canonical Effect package shape - which directory/file owns which Effect export (domain, services, adapters, workflows, CLI/HTTP boundaries, tests, Alchemy deploy) - see the effect-package-map.md file next to this SKILL.md. Use it when scaffolding a new package or reviewing where an Effect import landed.5152## Upstream Reference5354Use `reference/effect` or `effect-solutions` CLI when you need current Effect source or examples instead of relying on memory:55- `reference/effect/AGENTS.md` — upstream repository rules, including pnpm validation commands, generated barrels, changesets, and `it.effect` conventions.56- `reference/effect/packages/effect/` — core library source and tests.57- `reference/effect/packages/platform-bun/` and `reference/effect/packages/platform-node/` — runtime/platform examples.58- `reference/effect/packages/vitest/` — Effect-aware Vitest helpers.5960If the submodule is missing in a fresh checkout, initialize it before using local references:6162```bash63git submodule update --init skills/effect-typescript/reference/effect64```6566For 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.6768## JSON Encoding & Decoding6970Use `Schema.fromJsonString` to parse JSON strings and validate them with your schema in one step. This combines `JSON.parse` + schema decoding in one step, and `JSON.stringify` + schema encoding for the reverse:7172```typescript73import { Effect, Schema } from "effect"7475const Row = Schema.Literals(["A", "B", "C", "D", "E", "F", "G", "H"])76const Column = Schema.Literals(["1", "2", "3", "4", "5", "6", "7", "8"])7778class Position extends Schema.Class<Position>("Position")({79 row: Row,80 column: Column,81}) {}8283class Move extends Schema.Class<Move>("Move")({84 from: Position,85 to: Position,86}) {}8788// fromJsonString combines JSON.parse + schema decoding89// MoveFromJson is a schema that takes a JSON string and returns a Move90const MoveFromJson = Schema.fromJsonString(Move)9192const program = Effect.gen(function* () {93 // Parse and validate JSON string in one step94 // Use MoveFromJson (not Move) to decode from JSON string95 const jsonString = '{"from":{"row":"A","column":"1"},"to":{"row":"B","column":"2"}}'96 const move = yield* Schema.decodeUnknownEffect(MoveFromJson)(jsonString)9798 yield* Effect.log("Decoded move", move)99100 // Encode to JSON string in one step (typed as string)101 // Use MoveFromJson (not Move) to encode to JSON string102 const json = yield* Schema.encodeEffect(MoveFromJson)(move)103 return json104})105```