From effect-ts
This skill should be used when the user asks about "Effect services", "dependency injection", "Effect.Tag", "Context.Tag", "Layer", "Effect.provide", "Effect.provideService", "service implementation", "managing dependencies", "Layer.succeed", "Layer.effect", "Layer.scoped", "composing layers", "Layer.merge", "Layer.provide", "default services", "layer memoization", "testability", "test layers", "mock services", or needs to understand how Effect handles the Requirements (R) type parameter.
npx claudepluginhub andrueandersoncs/claude-skill-effect-ts --plugin effect-tsThis skill uses the workspace's default tool permissions.
The third type parameter in `Effect<A, E, R>` represents **requirements** - services and dependencies the effect needs to run:
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
The third type parameter in Effect<A, E, R> represents requirements - services and dependencies the effect needs to run:
Effect<Success, Error, Requirements>;
// ^^^^^^^^^^^^ Services needed
Effect uses a powerful dependency injection system based on Context and Layer.
The primary reason to define services is testability. Every external dependency (API calls, databases, file systems, third-party SDKs) MUST be wrapped in a Context.Tag service so that tests can provide a test implementation instead of hitting real systems. This is how Effect achieves 100% test coverage — business logic depends only on service interfaces, and tests swap in test layers that control all I/O.
import { Effect, Context } from "effect";
// Define service interface and tag together
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: string) => Effect.Effect<User, UserNotFound>;
readonly save: (user: User) => Effect.Effect<void>;
}
>() {}
// Using the service
const program = Effect.gen(function* () {
const repo = yield* UserRepository;
const user = yield* repo.findById("123");
return user;
});
// Type: Effect<User, UserNotFound, UserRepository>
interface UserRepository {
readonly findById: (id: string) => Effect.Effect<User, UserNotFound>;
}
const UserRepository = Context.Tag<UserRepository>("UserRepository");
const program = Effect.gen(function* () {
const userRepo = yield* UserRepository;
const emailService = yield* EmailService;
const user = yield* userRepo.findById(userId);
yield* emailService.send(user.email, "Welcome!");
});
Layers are recipes for building services:
const LoggerLive = Layer.succeed(Logger, {
log: (msg) => Effect.sync(() => console.log(msg)),
});
const ConfigLive = Layer.effect(
Config,
Effect.gen(function* () {
const env = yield* Effect.sync(() => process.env);
return {
apiUrl: env.API_URL ?? "http://localhost:3000",
debug: env.DEBUG === "true",
};
}),
);
const DatabaseLive = Layer.scoped(
Database,
Effect.gen(function* () {
const pool = yield* Effect.acquireRelease(createPool(), (pool) => Effect.promise(() => pool.end()));
return {
query: (sql) => Effect.promise(() => pool.query(sql)),
};
}),
);
const HttpClientLive = Layer.function(HttpClient, (baseUrl: string) => ({
get: (path) => Effect.tryPromise(() => fetch(baseUrl + path)),
}));
const program = getUserById("123");
const runnable = program.pipe(Effect.provide(AppLive));
await Effect.runPromise(runnable);
const runnable = program.pipe(
Effect.provideService(UserRepository, {
findById: (id) => Effect.succeed(mockUser),
save: (user) => Effect.void,
}),
);
const InfraLive = Layer.merge(DatabaseLive, LoggerLive);
const UserRepositoryLive = Layer.effect(
UserRepository,
Effect.gen(function* () {
const db = yield* Database;
return {
findById: (id) => db.query(`SELECT * FROM users WHERE id = ${id}`),
};
}),
);
const FullUserRepo = UserRepositoryLive.pipe(Layer.provide(DatabaseLive));
const Combined = UserRepositoryLive.pipe(Layer.provideMerge(DatabaseLive));
const InfraLive = Layer.mergeAll(DatabaseLive, LoggerLive, HttpClientLive);
const RepositoryLive = Layer.mergeAll(UserRepositoryLive, OrderRepositoryLive).pipe(Layer.provide(InfraLive));
const ServiceLive = Layer.mergeAll(UserServiceLive, OrderServiceLive).pipe(Layer.provide(RepositoryLive));
const AppLive = ServiceLive.pipe(Layer.provide(InfraLive));
Layers are memoized by default - each service is created once:
const AppLive = Layer.mergeAll(UserServiceLive, OrderServiceLive).pipe(Layer.provide(DatabaseLive));
const FreshDatabase = Layer.fresh(DatabaseLive);
Effect provides default implementations for common services:
const program = Effect.gen(function* () {
const now = yield* Clock.currentTimeMillis;
const random = yield* Random.next;
});
import { TestClock } from "effect";
const testProgram = program.pipe(Effect.provide(TestClock.layer));
Every service MUST have a test layer. This is how you achieve complete test coverage without hitting real external systems.
Use Layer.succeed for services that don't need to track state:
const EmailServiceTest = Layer.succeed(EmailService, {
send: (to, subject, body) => Effect.void, // No-op in tests
sendBulk: (recipients, subject, body) => Effect.void,
});
Use Layer.effect with Ref for services that need to maintain state:
const UserRepositoryTest = Layer.effect(
UserRepository,
Effect.gen(function* () {
const store = yield* Ref.make<Map<string, User>>(new Map());
return {
findById: (id: string) =>
Effect.gen(function* () {
const users = yield* Ref.get(store);
return yield* Option.match(Option.fromNullable(users.get(id)), {
onNone: () => Effect.fail(new UserNotFound({ userId: id })),
onSome: Effect.succeed,
}).pipe(Effect.flatten);
}),
save: (user: User) => Ref.update(store, (m) => new Map(m).set(user.id, user)),
};
}),
);
const TestEnv = Layer.mergeAll(UserRepositoryTest, EmailServiceTest, PaymentGatewayTest);
import { it, expect, layer } from "@effect/vitest";
import { Effect } from "effect";
layer(TestEnv)("UserService", (it) => {
it.effect("should create user and send welcome email", () =>
Effect.gen(function* () {
const repo = yield* UserRepository;
const email = yield* EmailService;
const user = new User({ id: "1", name: "Alice", email: "alice@test.com" });
yield* repo.save(user);
yield* email.send(user.email, "Welcome!", "Hello Alice");
const found = yield* repo.findById("1");
expect(found.name).toBe("Alice");
}),
);
});
The ultimate testing pattern — service test layers control all I/O, Arbitrary generates all data:
layer(TestEnv)("UserService Properties", (it) => {
it.effect.prop("should save and retrieve any valid user", [Arbitrary.make(User)], ([user]) =>
Effect.gen(function* () {
const repo = yield* UserRepository;
yield* repo.save(user);
const found = yield* repo.findById(user.id);
expect(found).toEqual(user);
}),
);
});
*Live for production layers, *Test for test layers (e.g., UserRepositoryLive, UserRepositoryTest)Layer.effect with Ref for stateful test layers - Repositories and caches need state trackingFor comprehensive requirements management documentation, consult ${CLAUDE_PLUGIN_ROOT}/references/llms-full.txt.
Search for these sections: