The R type parameter tracks what an Effect NEEDS to run. This is Effect's built-in dependency injection — no framework needed. In TypeScript you'd wire dependencies manually or use a DI container. Effect makes dependencies part of the type system.
Effect.Servicesucceed optioneffect optionR type parameteryield* ServiceTagContext.GenericTagdependencies optionEffect.provideService(tag, impl)// TypeScript: manual dependency wiring
interface Database {
query(sql: string): Promise<unknown[]>
}
function createDatabase(pool: Pool): Database {
return { query: (sql) => pool.query(sql) }
}
// Caller must remember to pass the right pool.
// Nothing prevents passing the wrong one.
// Nothing tracks which functions need a Database.
const db = createDatabase(pool)
const users = await db.query("SELECT * FROM users")import { Effect, Context } from "effect"
// === Option 1: Effect.Service (recommended) ===
class Database extends Effect.Service<Database>()("Database", {
effect: Effect.gen(function* () ({
query: (sql: string) => Effect.tryPromise(() => pool.query(sql))
})),
dependencies: [ConnectionPool.Default]
}) {}
// Auto-generated:
// Database.Default — includes all dependencies
// Database.DefaultWithoutDependencies — requires them externally
const getUsers = Effect.gen(function* () {
const db = yield* Database // pulls from context, adds Database to R
return yield* db.query("SELECT * FROM users")
})
// Type: Effect<User[], SqlError, Database>
// === Option 2: Context.GenericTag (lower-level) ===
interface Logger {
readonly log: (msg: string) => Effect.Effect<void>
}
const Logger = Context.GenericTag<Logger>("Logger")
const program = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("hello")
})
// Type: Effect<void, never, Logger>
// Provide and run
program.pipe(
Effect.provideService(Logger, {
log: (msg) => Effect.sync(() => console.log(msg))
}),
Effect.runPromise
) TS interface Effect Service
───────────── ──────────────
interface Logger { class Logger extends
log(msg: string): void Effect.Service<Logger>()("Logger", {
} succeed: { log: ... }
}) {}
│ │
│ ├── Tag (unique identity)
│ ├── .Default (auto Layer)
│ └── R type tracking
│ │
▼ ▼
You wire it manually. Compiler enforces it:
Forget? Runtime error. R ≠ never? Won't compile.
┌─────────────────────────────────────────────────────────┐
│ yield* Logger ← pulls Logger from context │
│ │ │
│ └──► adds Logger to R automatically │
│ │
│ Effect<void, never, Logger> │
│ ^^^^^^ │
│ "this needs a Logger to run" │
│ │
│ pipe(Effect.provideService(Logger, impl)) │
│ │
│ Effect<void, never, never> │
│ ^^^^^ │
│ "all dependencies satisfied — can run" │
└─────────────────────────────────────────────────────────┘Create a Clock service using Effect.Service that has a single method `now` returning an Effect with the current timestamp. Then write a program that uses the service.
import { Effect } from "effect"
// TODO: Define a Clock service with Effect.Service
// It should have a method: now: () => Effect.Effect<number>
// Use Effect.sync(() => Date.now()) in the implementation
// TODO: Write a program that uses Clock to get the current time
// const program = Effect.gen(function* () { ... })import { Effect } from "effect"
class Clock extends Effect.Service<Clock>()("Clock", {
succeed: {
now: () => Effect.sync(() => Date.now())
}
}) {}
const program = Effect.gen(function* () {
const clock = yield* Clock
const time = yield* clock.now()
return time
})
// Type: Effect<number, never, Clock>
// Run it:
// program.pipe(Effect.provide(Clock.Default), Effect.runPromise)Given the Clock service below, provide a fake implementation that always returns 0. Use Effect.provideService to swap the real implementation for a test one.
import { Effect } from "effect"
class Clock extends Effect.Service<Clock>()("Clock", {
succeed: {
now: () => Effect.sync(() => Date.now())
}
}) {}
const program = Effect.gen(function* () {
const clock = yield* Clock
return yield* clock.now()
})
// TODO: provide a fake Clock that always returns 0
// and run the program
const test = program.pipe(
// your code here
)import { Effect } from "effect"
class Clock extends Effect.Service<Clock>()("Clock", {
succeed: {
now: () => Effect.sync(() => Date.now())
}
}) {}
const program = Effect.gen(function* () {
const clock = yield* Clock
return yield* clock.now()
})
const test = program.pipe(
Effect.provideService(Clock, {
now: () => Effect.succeed(0)
}),
Effect.runPromise
)
// test resolves to 0 — no real clock involvedCreate a Random service using Context.GenericTag (not Effect.Service). Define the interface, the tag, a program that uses it, and provide an implementation.
import { Effect, Context } from "effect"
// TODO: Define a Random interface with:
// next: () => Effect.Effect<number>
// TODO: Create a tag with Context.GenericTag
// TODO: Write a program that calls random.next()
// TODO: Provide an implementation and run itimport { Effect, Context } from "effect"
interface Random {
readonly next: () => Effect.Effect<number>
}
const Random = Context.GenericTag<Random>("Random")
const program = Effect.gen(function* () {
const random = yield* Random
return yield* random.next()
})
// Type: Effect<number, never, Random>
const result = program.pipe(
Effect.provideService(Random, {
next: () => Effect.sync(() => Math.random())
}),
Effect.runPromise
)In Step 11 you used Effect.provideService to give one service at a time. That works for simple cases. But real apps have chains: Database needs a ConnectionPool, which needs Config. Layers let you describe these chains once, and Effect wires them for you — in the right order, creating each service once. Key distinction: a Service is WHAT exists (the interface), a Layer is HOW to build it (the recipe). Effect.Service bundles both — that's why you already have layers via MyService.Default without writing any Layer code.
MyService.DefaultLayer.succeed(tag, value)Layer.effect(tag, effect)Layer.merge(a, b)Layer.provide(source)Effect.provide(layer)Memoization// TypeScript: you wire dependencies by hand in main()
function createConfig() {
return { logLevel: "INFO", connection: "postgres://..." }
}
function createLogger(config: Config) {
return { log: (msg: string) => console.log(`[${config.logLevel}] ${msg}`) }
}
function createDatabase(config: Config, logger: Logger) {
return { query: (sql: string) => { logger.log(sql); /* ... */ } }
}
// The order matters. Forget a step? Runtime error.
// Two things need config? You pass it twice manually.
const config = createConfig()
const logger = createLogger(config)
const db = createDatabase(config, logger)import { Effect, Context, Layer } from "effect"
// ── 1. Layer.succeed: wrap a plain value ──
// Use for config, constants — things that don't need setup
class Config extends Context.Tag("Config")<Config, {
readonly logLevel: string
readonly connection: string
}>() {}
const ConfigLive = Layer.succeed(Config, {
logLevel: "INFO",
connection: "postgres://localhost/mydb"
})
// Layer<Config, never, never>
// "I provide Config. I need nothing."
// ── 2. Layer.effect: build a service that needs other services ──
class Logger extends Context.Tag("Logger")<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>() {}
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config // ← I need Config to build Logger
return {
log: (msg) => Effect.sync(() =>
console.log(`[${config.logLevel}] ${msg}`)
)
}
})
)
// Layer<Logger, never, Config>
// "I provide Logger. I need Config."
// ── 3. Layer.provide: satisfy a layer's dependencies ──
// LoggerLive needs Config. Give it ConfigLive.
const LoggerResolved = LoggerLive.pipe(Layer.provide(ConfigLive))
// Layer<Logger, never, never> ← resolved! No more dependencies.
// ── 4. Layer.merge: bundle resolved layers together ──
// Your program needs both Config AND Logger?
// Merge the resolved layers into one:
const AppLive = Layer.merge(ConfigLive, LoggerResolved)
// Layer<Config | Logger, never, never>
// Both resolved. Nothing left to wire.
// ── 5. Putting it all together ──
class Database extends Context.Tag("Database")<Database, {
readonly query: (sql: string) => Effect.Effect<unknown>
}>() {}
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
return {
query: (sql) => Effect.gen(function* () {
yield* logger.log(`Executing: ${sql}`)
return { result: `data from ${config.connection}` }
})
}
})
)
// Layer<Database, never, Config | Logger>
// Database needs Config+Logger. AppLive provides both.
const MainLive = DatabaseLive.pipe(Layer.provide(AppLive))
// Layer<Database, never, never> ← fully resolved!
const program = Effect.gen(function* () {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
Effect.runPromise(Effect.provide(program, MainLive)) Step 1: Each layer is a recipe
─────────────────────────────────────────────────────
ConfigLive LoggerLive
┌──────────────────┐ ┌──────────────────────┐
│ Layer.succeed │ │ Layer.effect │
│ │ │ │
│ provides: Config │ │ provides: Logger │
│ needs: nothing │ │ needs: Config ←── │── "I can't build
└──────────────────┘ └──────────────────────┘ without Config"
Step 2: Resolve dependencies (Layer.provide)
─────────────────────────────────────────────────────
LoggerLive.pipe(Layer.provide(ConfigLive))
│
"here's the Config you need"
│
▼
LoggerResolved
┌──────────────────────┐
│ provides: Logger │
│ needs: nothing ✓ │
└──────────────────────┘
Step 3: Bundle together (Layer.merge)
─────────────────────────────────────────────────────
Layer.merge(ConfigLive, LoggerResolved)
│
▼
AppLive
┌──────────────────────┐
│ provides: Config │
│ Logger │
│ needs: nothing ✓ │
└──────────────────────┘
Step 4: Wire to your program (Effect.provide)
─────────────────────────────────────────────────────
program ──── needs Config + Logger
│
│ Effect.provide(program, AppLive)
│
▼
runnable ── needs nothing → Effect.runPromise(runnable)Most of the time you don't write layers manually. Create a Logger and a Metrics service with Effect.Service, where Metrics depends on Logger. Then provide Metrics.Default to a program.
import { Effect } from "effect"
// TODO: Create Logger with Effect.Service
// It should have: log(msg: string) => Effect.Effect<void>
// TODO: Create Metrics with Effect.Service
// It should have: track(event: string) => Effect.Effect<void>
// Metrics should depend on Logger (use dependencies option)
// TODO: Write a program that uses Metrics, provide Metrics.Defaultimport { Effect } from "effect"
class Logger extends Effect.Service<Logger>()("Logger", {
succeed: {
log: (msg: string) => Effect.sync(() => console.log(msg))
}
}) {}
class Metrics extends Effect.Service<Metrics>()("Metrics", {
effect: Effect.gen(function* () {
const logger = yield* Logger
return {
track: (event: string) => logger.log(`[metric] ${event}`)
}
}),
dependencies: [Logger.Default]
}) {}
const program = Effect.gen(function* () {
const metrics = yield* Metrics
yield* metrics.track("page_view")
})
// Metrics.Default includes Logger automatically
Effect.runPromise(program.pipe(Effect.provide(Metrics.Default)))Create a Config service using Context.Tag and a ConfigLive layer using Layer.succeed. Config has a port (number) and host (string).
import { Effect, Context, Layer } from "effect"
// TODO: Define Config with Context.Tag
// TODO: Create ConfigLive with Layer.succeedimport { Effect, Context, Layer } from "effect"
class Config extends Context.Tag("Config")<Config, {
readonly port: number
readonly host: string
}>() {}
const ConfigLive = Layer.succeed(Config, {
port: 3000,
host: "localhost"
})
// Layer<Config, never, never>Given Config below, create a Server service that depends on Config. Use Layer.effect. Then use Layer.provide to plug ConfigLive into ServerLive.
import { Effect, Context, Layer } from "effect"
class Config extends Context.Tag("Config")<Config, {
readonly port: number
readonly host: string
}>() {}
const ConfigLive = Layer.succeed(Config, {
port: 3000,
host: "localhost"
})
// TODO: Define Server service (with a start() method returning Effect<void>)
// TODO: Create ServerLive with Layer.effect — yield* Config inside
// TODO: Use Layer.provide to give ConfigLive to ServerLiveimport { Effect, Context, Layer } from "effect"
class Config extends Context.Tag("Config")<Config, {
readonly port: number
readonly host: string
}>() {}
const ConfigLive = Layer.succeed(Config, {
port: 3000,
host: "localhost"
})
class Server extends Context.Tag("Server")<Server, {
readonly start: () => Effect.Effect<void>
}>() {}
const ServerLive = Layer.effect(
Server,
Effect.gen(function* () {
const config = yield* Config
return {
start: () => Effect.sync(() =>
console.log(`Listening on ${config.host}:${config.port}`)
)
}
})
)
// Layer<Server, never, Config> — needs Config
// Plug ConfigLive into ServerLive:
const ServerResolved = ServerLive.pipe(Layer.provide(ConfigLive))
// Layer<Server, never, never> — fully resolved!You have ConfigLive and LoggerLive (which depends on Config). Your program needs both Config and Logger. First resolve LoggerLive's dependency, then merge both resolved layers.
import { Effect, Context, Layer } from "effect"
class Config extends Context.Tag("Config")<Config, {
readonly env: string
}>() {}
const ConfigLive = Layer.succeed(Config, { env: "production" })
class Logger extends Context.Tag("Logger")<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>() {}
const LoggerLive = Layer.effect(Logger, Effect.gen(function* () {
const config = yield* Config
return { log: (msg) => Effect.sync(() => console.log(`[${config.env}] ${msg}`)) }
}))
// TODO: 1. Resolve LoggerLive by providing ConfigLive to it
// TODO: 2. Merge ConfigLive and the resolved Logger layer
// TODO: 3. Provide AppLive to the program
const program = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("hello")
})import { Effect, Context, Layer } from "effect"
class Config extends Context.Tag("Config")<Config, {
readonly env: string
}>() {}
const ConfigLive = Layer.succeed(Config, { env: "production" })
class Logger extends Context.Tag("Logger")<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>() {}
const LoggerLive = Layer.effect(Logger, Effect.gen(function* () {
const config = yield* Config
return { log: (msg) => Effect.sync(() => console.log(`[${config.env}] ${msg}`)) }
}))
// 1. Resolve LoggerLive's dependency on Config
const LoggerResolved = LoggerLive.pipe(Layer.provide(ConfigLive))
// Layer<Logger, never, never> ← no more dependencies
// 2. Merge two resolved layers
const AppLive = Layer.merge(ConfigLive, LoggerResolved)
// Layer<Config | Logger, never, never> ← both ready!
const program = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("hello")
})
Effect.runPromise(Effect.provide(program, AppLive))