You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
EXPLANATION.md — detailed writeup with logs and illustration
Steps
From repo root, run bun scratchpad/rpc-handler-type-safety-pitfall/mod.ts.
Then run bun scratchpad/rpc-handler-type-safety-pitfall/mod-fixed.ts.
Expected
mod.ts: TypeScript accepts the _typeTest assignment, so the handler layer appears to need never.
Runtime fails with a missing service error because MyService is not provided.
mod-fixed.ts: _typeTest requires MyService, and runtime succeeds when the layer is provided.
Why This Repro Matches The Pitfall
MyRpcs.toLayer(Effect.succeed(handlers)) erases the handler requirements, so the layer type does not surface MyService.
When RpcGroup.toLayer is called with Effect.succeed(handlers), the handler effect’s
requirements (R) are erased. This compiles even if a handler uses a service that is
never provided, and it fails at runtime with “Service not found”.
This repro shows:
Unsafe pattern: compiles, but fails at runtime
Safe pattern: surfaces requirements in types and runs correctly
Repro layout
mod.ts — unsafe pattern
mod-fixed.ts — safe pattern
Steps
From the repo root:
bun scratchpad/rpc-handler-type-safety-pitfall/mod.ts
bun scratchpad/rpc-handler-type-safety-pitfall/mod-fixed.ts
Expected vs actual
1) Unsafe pattern (mod.ts)
TypeScript accepts this type test, suggesting no requirements:
But at runtime, the handler uses MyService, which is never provided.
Runtime failure log (excerpt):
timestamp=2025-12-28T12:22:55.969Z level=ERROR fiber=#0 cause="Error: Service not found: @repro/MyService (defined at /.../mod.ts:4:55)"
error: Service not found: @repro/MyService (defined at /.../mod.ts:4:55)
2) Safe pattern (mod-fixed.ts)
The layer requirements are surfaced, and the program succeeds when the service is
provided:
RpcGroup.of validates handler keys, but it doesn’t track handler return types or
requirements. The handler object is returned as-is.
Effect.succeed(handlers) erases requirements
Effect.succeed has type Effect<Handlers, never, never>, so the requirement
channel R becomes never, even if handlers internally require services.
HandlersContext inference can collapse to never
RpcGroup.toLayer tries to re-derive requirements from handler return types using
nested conditional types. When the compiler can’t resolve those, it often falls back
to never rather than producing an error.
Illustration
Handlers (uses MyService)
|
| RpcGroup.of (identity) Effect.succeed
| (validates keys only) (R = never)
v v
handlers object -----------------> Effect<handlers, never, never>
|
v
RpcGroup.toLayer
(R often inferred as never)
Fix pattern
Lift service acquisition into the layer construction effect:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters