Problem
Opt-in rule explicitly requested by the maintainers on stream: flags calls to the v4 tag accessor helpers use and useSync. Calling an accessor immediately adds the service to the requirements of the produced effect, so when used inside another service's method implementation, that service's public shape silently absorbs the dependency. The recommended idiom is to yield the service once at construction and call methods on the resolved instance.
Why the compiler is silent / what breaks at runtime: Everything type-checks, but the service's method type now carries the transitive dependency, so every consumer inherits a requirement that describes the implementation rather than the contract — 'the shape of the service should be just what you do and not what it requires to do it'. The team said outright: 'we could probably add an error rule in the LSP. You can opt in and say no use and no use sync.'
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: no-service-use-accessor
// BAD: calling the tag accessor helpers `use` / `useSync` inside another
// service's method implementation immediately adds Notifications to the R of
// the produced effect, so Mailer's inferred shape silently absorbs the
// dependency — every consumer of Mailer.send now inherits Notifications, a
// requirement that describes the implementation instead of the contract.
// The linter would flag `Notifications.use(...)` and `Notifications.useSync(...)`.
import { Context, Effect, Layer } from "effect"
class Notifications extends Context.Service<Notifications, {
readonly notify: (msg: string) => Effect.Effect<void>
}>()("Notifications") {
static Default = Layer.succeed(this, {
notify: (_msg: string) => Effect.void
})
}
class Mailer extends Context.Service<Mailer>()("Mailer", {
make: Effect.gen(function*() {
return {
// BAD: accessor call — R of `send` is now `Notifications`
send: (msg: string) => Notifications.use((n) => n.notify(msg)),
// BAD: the sync accessor variant leaks in exactly the same way
channelName: () => Notifications.useSync((_n) => "default-channel")
}
})
}) {
static Default = Layer.effect(this, this.make)
}
// The leak made visible: the caller only asked for Mailer, yet its
// requirements must also mention Notifications to type-check.
const program: Effect.Effect<void, never, Mailer | Notifications> = Effect.gen(function*() {
const mailer = yield* Mailer
yield* mailer.send("hello")
})
void program
Good
// RULE: no-service-use-accessor
// GOOD: yield the dependency once at construction time and call methods on the
// resolved instance. The dependency is settled when the Mailer layer is built,
// so the methods' R stays clean — the service's shape is "just what you do,
// not what it requires to do it". Consumers depend only on Mailer.
import { Context, Effect, Layer } from "effect"
class Notifications extends Context.Service<Notifications, {
readonly notify: (msg: string) => Effect.Effect<void>
}>()("Notifications") {
static Default = Layer.succeed(this, {
notify: (_msg: string) => Effect.void
})
}
class Mailer extends Context.Service<Mailer>()("Mailer", {
make: Effect.gen(function*() {
// GOOD: dependency captured once at construction
const notifications = yield* Notifications
return {
// R of `send` stays never — no leak into the public shape
send: (msg: string) => notifications.notify(msg),
channelName: () => Effect.succeed("default-channel")
}
})
}) {
// The requirement lives in the layer graph, where it belongs.
static DefaultWithoutDependencies = Layer.effect(this, this.make)
static Default = this.DefaultWithoutDependencies.pipe(
Layer.provide(Notifications.Default)
)
}
// Consumers now only require Mailer — the contract, not the implementation.
const program: Effect.Effect<void, never, Mailer> = Effect.gen(function*() {
const mailer = yield* Mailer
yield* mailer.send("hello")
})
void program
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
noServiceUseAccessor
Problem
Opt-in rule explicitly requested by the maintainers on stream: flags calls to the v4 tag accessor helpers use and useSync. Calling an accessor immediately adds the service to the requirements of the produced effect, so when used inside another service's method implementation, that service's public shape silently absorbs the dependency. The recommended idiom is to yield the service once at construction and call methods on the resolved instance.
Why the compiler is silent / what breaks at runtime: Everything type-checks, but the service's method type now carries the transitive dependency, so every consumer inherits a requirement that describes the implementation rather than the contract — 'the shape of the service should be just what you do and not what it requires to do it'. The team said outright: 'we could probably add an error rule in the LSP. You can opt in and say no use and no use sync.'
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
noServiceUseAccessor