Effect-TS Developer Guide

SkillDev tools

Expert guide for writing Effect-TS code, including project setup, core principles, data modeling with Schema, error handling, and the Context.Tag service pattern. Use when writing, refactoring, or analyzing TypeScript code using the Effect library.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Effect-TS Developer Guide skill

What this skill tells your AI

The instructions your AI receives, as published by pedronauck/skills in skills/curated/effect-ts/SKILL.md and read by ahel’s review.

Guidelines, patterns, and best practices for Effect-TS in this project.

Reference Documents

Read the relevant reference before writing code. references/core-patterns.md is the master index.

ReferenceTopics
references/foundations.mdSetup, imports, TypeScript config
references/construction-and-style.mdEffect.gen, pipe, Effect.fn, Effect.fnUntraced
references/schema-errors-config.mdSchema modeling, errors, config, retry
references/pattern-matching.mdMatch.type, Match.value, Match.tag, Match.exhaustive — mandatory for tagged unions
references/control-flow-and-runtime.mdEffect.if, Effect.when, loops, runSync, runPromise, ManagedRuntime
references/data-types.mdAll data types: Option, Either, Data, Exit, Cause, Duration, DateTime, BigDecimal, Chunk, HashSet, Redacted
references/data-and-testing.mdOption/Either/Array quick ref, @effect/vitest setup
references/concurrency-and-resources.mdConcurrency, Scope, finalizers, resources
references/streams-deep-dive.mdCreating, operations, grouping, partitioning, broadcasting, buffering, throttling, error handling
references/sink.mdSink constructors, collecting, folding, operations, concurrency, leftovers, Stream.transduce
references/batching-and-caching.mdRequest batching (RequestResolver), cachedWithTTL
references/schema-transforms-and-filters.mdSchema.transform, Schema.filter, refinements
references/api-platform-observability.mdHttpApi, logging, tracing, spans
references/class-patterns.mdContext.Tag service pattern, layers, memoization, testing
references/error-handling-patterns.mdData.TaggedError, Schema.TaggedError, error composition, recovery
references/library-development-patterns.mdForbidden patterns, Effect.fn vs Effect.fnUntraced, resource management
references/testing-patterns.md@effect/vitest with assert, TestClock, service mocking
references/quality-tooling-and-resources.mdAnti-patterns, validation checklist, packages

Core Principles

  1. Effect is not just for async — Use Effect for any fallible operation
  2. Immutability — Use Effect's immutable data structures (Data, Chunk, HashSet)
  3. Type Safety — Track errors in types. No any or unknown in error channels
  4. Composition — Build programs by composing small Effects
  5. Schema-First — Define data models using Schema with branded types
  6. Pattern Matching — Use Match for all branching over tagged unions (never switch/if-else on _tag)
  7. Effect Data Types — Use Option (not null), Either (not ad-hoc), Duration (not raw ms), DateTime (not Date), BigDecimal (not floats), Redacted (for secrets). See references/data-types.md

Quick Reference

Import Convention

import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as Match from "effect/Match";
import * as Option from "effect/Option";
import * as Either from "effect/Either";
import * as Data from "effect/Data";
import * as Duration from "effect/Duration";
import { pipe } from "effect/Function";
// Also: DateTime, BigDecimal, Chunk, HashSet, Exit, Cause, Redacted

Effect.gen vs pipe vs Effect.fn

// Effect.gen — complex logic with branching
Effect.gen(function* () {
  const user = yield* fetchUser(id);
  if (user.isAdmin) yield* logAdminAccess(user);
  return user;
});

// pipe — linear transformations
pipe(fetchData(), Effect.map(transform), Effect.flatMap(save));

// Effect.fn — traced reusable functions (public API)
const processUser = Effect.fn("processUser")(function* (userId: string) {
  const user = yield* getUser(userId);
  return yield* processData(user);
});

Error Handling

Data.TaggedError for in-process discrimination, Schema.TaggedError for serializable errors. See references/error-handling-patterns.md.

export class NotFoundError extends Data.TaggedError("NotFoundError")<{
  id: string;
}> {}

// Recovery
pipe(riskyOp, Effect.catchTag("NotFoundError", (e) => Effect.succeed(null)));

Pattern Matching (Mandatory for Tagged Unions)

Always use MatchMatch.exhaustive catches missing cases at compile time. See references/pattern-matching.md.

// Match.type — reusable matcher function
const handle = Match.type<Status>().pipe(
  Match.tag("Pending", (s) => `Pending since ${s.requestedAt}`),
  Match.tag("Approved", (s) => `Approved by ${s.approvedBy}`),
  Match.exhaustive // Compile error if any variant is missing
);

// Match.valueTags — shorthand for immediate matching
Match.valueTags(status, {
  Pending: (s) => `Pending since ${s.requestedAt}`,
  Approved: (s) => `Approved by ${s.approvedBy}`,
});

Service Pattern (Context.Tag)

See references/class-patterns.md for full pattern with factory methods and layers.

export class MyService extends Context.Tag("@myapp/MyService")<
  MyService,
  { readonly find: (id: string) => Effect.Effect<Result, NotFoundError> }
>() {
  static readonly layer = Layer.effect(MyService, Effect.gen(function* () {
    const db = yield* Database;
    return MyService.of({ find: MyService.createFind(db) });
  }));
}

Testing

CRITICAL: Use assert from @effect/vitest for it.effect. Never expect with it.effect. See references/testing-patterns.md.

import { assert, describe, it } from "@effect/vitest";

it.effect("processes data", () =>
  Effect.gen(function* () {
    const result = yield* processData("input");
    assert.strictEqual(result, "expected");
  }).pipe(Effect.provide(MyService.testLayer))
);

Forbidden Patterns

// NEVER: try-catch in Effect.gen — use Effect.exit instead
Effect.gen(function* () {
  try { yield* someEffect } catch (e) { } // WRONG — will never catch
});

// NEVER: Type assertions
const value = something as any;   // FORBIDDEN
const value = something as never; // FORBIDDEN

// NEVER: Missing return on terminal yield
Effect.gen(function* () {
  if (bad) { yield* Effect.fail("err") } // Missing return!
});

// NEVER: switch/if-else on _tag — use Match instead
switch (status._tag) { /* no exhaustiveness checking! */ }

// NEVER: Effect.runSync inside Effects
Effect.gen(function* () { Effect.runSync(sideEffect) }); // Loses error tracking

// NEVER: Native JS where Effect data types exist
const x: string | null = null;              // Use Option<string>
const delay = 5000;                         // Use Duration.seconds(5)
const now = new Date();                     // Use DateTime.now or DateTime.unsafeNow()
const price = 0.1 + 0.2;                   // Use BigDecimal for precision
const secret = "sk-1234";                   // Use Redacted.make("sk-1234")

// NEVER: expect with it.effect
it.effect("test", () => Effect.gen(function* () {
  expect(result).toBe(value) // WRONG — use assert.strictEqual
}));

// NEVER: Inline layers (breaks memoization)
Layer.provide(Postgres.layer({ url })) // Store in constant instead

Validation Checklist

  • Imports use import * as Module from "effect/Module"
  • Effect.gen for complex logic, pipe for linear, Effect.fn for public API
  • Match for all _tag branching with Match.exhaustive
  • Branded types for domain primitives (IDs, Emails)
  • Errors: Data.TaggedError (discrimination) or Schema.TaggedError (serializable)
  • No any/unknown in error channels, no type assertions
  • No try-catch in Effect.gen — use Effect.exit
  • return yield* for terminal effects (Effect.fail, Effect.interrupt)
  • Services: Context.Tag with static factory methods and Effect.fn tracing
  • Layers: Layer.merge/Layer.provide, parameterized layers in constants
  • Resources: Effect.acquireRelease or Effect.scoped
  • Option for nullable values, Either for sync success/failure
  • Duration for time values, DateTime for dates (not Date)
  • BigDecimal for financial/precise math, Redacted for secrets
  • Data.struct/Data.Class for structural equality, HashSet for sets
  • Clock.currentTimeMillis instead of Date.now()
  • Tests: assert from @effect/vitest (not expect) with it.effect
  • Run pnpm run typecheck and pnpm run test

Reference Implementation

See /packages/looper/src/data/api-client/api-client.ts for Context.Tag service pattern.

Effect Solutions CLI

pnpm exec effect-solutions list              # List all topics
pnpm exec effect-solutions show <slug...>    # Read topics
pnpm exec effect-solutions search <term>     # Search by keyword

Signals

GitHub stars
611
Forks
88
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
effect-ts-pedronauck
Source
github.com/pedronauck/skills