Problem
When a service declares a dependencies array, its generated .Default layer has those dependency layers pre-provided (locally eliminated). If a test composition then provides X.Default together with a different layer for one of those packed tags (e.g. a test implementation), the override never reaches X — the packed default was already provided internally. The rule detects a layer composition where Service.Default appears together with another layer producing a tag that is in that service's dependencies array, and suggests X.DefaultWithoutDependencies.
Why the compiler is silent / what breaks at runtime: Types are fine (Default already eliminated those requirements, so the extra provide is a no-op in the R channel), but at runtime the service keeps using the packed production dependency — a test silently talks to the real user database or SMS provider while the test layer is ignored.
Both examples below type-check with zero errors against effect@4.0.0-beta.104 (re-verified) under the monorepo's strict tsconfig, verified with an isolated per-proposal tsconfig — so the compiler offers no protection here and a diagnostic is the only static safety net.
Bad — compiles cleanly, the rule should flag this
// RULE: default-layer-overridden-dependency-ignored
// BAD: AlertService.Default already has its dependencies (UserService, SmsService)
// provided internally (locally eliminated). Providing UserService.testLayer on top
// type-checks — the R channel is already `never`, so the extra provide is a no-op —
// but at runtime AlertService keeps using the packed production UserService.
import { Context, Effect, Layer } from "effect"
class UserService extends Context.Service<UserService, {
readonly findPhone: (id: string) => Effect.Effect<string>
}>()("UserService") {
static Default = Layer.succeed(this, {
findPhone: (id: string) => Effect.succeed(`prod-phone-${id}`)
})
static testLayer = Layer.succeed(this, {
findPhone: (_id: string) => Effect.succeed("555-0000")
})
}
class SmsService extends Context.Service<SmsService, {
readonly send: (phone: string, msg: string) => Effect.Effect<void>
}>()("SmsService") {
static Default = Layer.succeed(this, {
send: (_phone: string, _msg: string) => Effect.void
})
}
class AlertService extends Context.Service<AlertService>()("AlertService", {
make: Effect.gen(function*() {
const users = yield* UserService
const sms = yield* SmsService
return {
alert: (userId: string, msg: string) =>
Effect.flatMap(users.findPhone(userId), (phone) => sms.send(phone, msg))
}
})
}) {
// "Fully wired" default: dependency layers are packed in and no longer
// visible in the layer's requirements.
static Default = Layer.effect(this, this.make).pipe(
Layer.provide([UserService.Default, SmsService.Default])
)
}
// BAD: compiles cleanly, but UserService.testLayer never reaches AlertService —
// the test silently talks to the real UserService implementation.
const TestLayer = AlertService.Default.pipe(
Layer.provide(UserService.testLayer)
)
void TestLayer
Good
// RULE: default-layer-overridden-dependency-ignored
// GOOD: compose the test layer from DefaultWithoutDependencies — the variant whose
// requirements are still open — so the provided test implementations are the ones
// AlertService actually receives at runtime.
import { Context, Effect, Layer } from "effect"
class UserService extends Context.Service<UserService, {
readonly findPhone: (id: string) => Effect.Effect<string>
}>()("UserService") {
static Default = Layer.succeed(this, {
findPhone: (id: string) => Effect.succeed(`prod-phone-${id}`)
})
static testLayer = Layer.succeed(this, {
findPhone: (_id: string) => Effect.succeed("555-0000")
})
}
class SmsService extends Context.Service<SmsService, {
readonly send: (phone: string, msg: string) => Effect.Effect<void>
}>()("SmsService") {
static Default = Layer.succeed(this, {
send: (_phone: string, _msg: string) => Effect.void
})
static testLayer = Layer.succeed(this, {
send: (_phone: string, _msg: string) => Effect.void
})
}
class AlertService extends Context.Service<AlertService>()("AlertService", {
make: Effect.gen(function*() {
const users = yield* UserService
const sms = yield* SmsService
return {
alert: (userId: string, msg: string) =>
Effect.flatMap(users.findPhone(userId), (phone) => sms.send(phone, msg))
}
})
}) {
// Un-wired variant: requirements (UserService | SmsService) stay open.
static DefaultWithoutDependencies = Layer.effect(this, this.make)
static Default = this.DefaultWithoutDependencies.pipe(
Layer.provide([UserService.Default, SmsService.Default])
)
}
// GOOD: the overrides genuinely satisfy AlertService's requirements.
const TestLayer = AlertService.DefaultWithoutDependencies.pipe(
Layer.provide([UserService.testLayer, SmsService.testLayer])
)
void TestLayer
Where this came up
Mined from the Effect Office Hours playlist; deduplicated against all implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
defaultLayerOverriddenDependencyIgnored
Problem
When a service declares a dependencies array, its generated .Default layer has those dependency layers pre-provided (locally eliminated). If a test composition then provides X.Default together with a different layer for one of those packed tags (e.g. a test implementation), the override never reaches X — the packed default was already provided internally. The rule detects a layer composition where Service.Default appears together with another layer producing a tag that is in that service's dependencies array, and suggests X.DefaultWithoutDependencies.
Why the compiler is silent / what breaks at runtime: Types are fine (Default already eliminated those requirements, so the extra provide is a no-op in the R channel), but at runtime the service keeps using the packed production dependency — a test silently talks to the real user database or SMS provider while the test layer is ignored.
Both examples below type-check with zero errors against
effect@4.0.0-beta.104(re-verified) under the monorepo's strict tsconfig, verified with an isolated per-proposal tsconfig — so the compiler offers no protection here and a diagnostic is the only static safety net.Bad — compiles cleanly, the rule should flag this
Good
Where this came up
Mined from the Effect Office Hours playlist; deduplicated against all implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
defaultLayerOverriddenDependencyIgnored