fix(server): keep union and index-signature context types across .use() - #2110
Conversation
MergedContext and MergedInitialContext used Omit, which collapses a union
to its shared keys and drops known keys beside an index signature. After
.use() added context, a discriminated union context could no longer be
narrowed and { [k: string]: unknown, db: string } lost db.
Replace Omit with a key-remapping mapped type, which distributes over
unions and keeps index signatures and known keys separately.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-lock
@orpc/experimental-msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
There was a problem hiding this comment.
ℹ️ Minor suggestion only — one edge case in the new distribution wrapper.
Reviewed changes
- Type-level context merge fix — replaced
Omitwith a homomorphicOmitKeysin bothMergedContextandMergedInitialContext, so discriminated unions and named keys beside index signatures survive.use(). - Union distribution —
MergedInitialContextnow distributes over a unionTInContext(TInContext extends any ? … : never), so each union member keeps its own extra keys instead of collapsing to the intersection of keys. - Tests — added union/index-signature type tests in
context.test-d.tsand an end-to-end.use()→.handler()narrowing test inbuilder.test-d.ts; updated three exact-type assertions that shifted from{}toobjectfor bare-objectcontexts.
I independently confirmed pnpm type:check passes across all packages and the root type tests, that the new tests fail to compile against the old definitions (so they pin the bug), and that the index-signature override path now yields db: number rather than string & number. The fix is sound; the only note is the never fallback below.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review at c0b20ccd, the only new commit (0553669b) is a refactor:
- Extracted
DistributiveOmitto@orpc/shared— the privateOmitKeyshelper inserver/context.tsis now the publicDistributiveOmit<T, K extends PropertyKey>inshared/src/types.ts, with a JSDoc note and unit type tests covering plain keys, readonly/optional modifiers, union distribution, and named keys beside an index signature. - Wired
server/context.tsto the shared helper —MergedContextandMergedInitialContextnow import it; the resolved types are unchanged fromc0b20ccd.
The one prior finding (the never fallback in MergedInitialContext) was withdrawn: never is unreachable through .use() and was not usable on main either, so it is not a regression. pnpm type:check passes for @orpc/shared and @orpc/server.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Context added by
.use()no longer breaks union or index-signature context types. Before,os.$context<{ kind: 'a', a: string } | { kind: 'b', b: number }>().use(({ next }) => next({ context: { user: 1 } }))gave the handler{ kind: 'a' | 'b' }, soif (context.kind === 'a') context.awas a tsc error. With{ [k: string]: unknown, db: string },context.dbbecameunknown. The handler now gets the full union, which narrows as expected, anddbstaysstring.Fixes
.use()adds context..use()..use()whose middleware needs a union context keeps that union in the procedure's initial context.call(procedure, input, { context: { kind: 'a', a: 'x' } })is no longer rejected.Performance
Omit. On 300 procedures with three.use()calls each: 5% fewer types and the same instantiation count (542,770 vs 543,993), with check time within noise.Reviewer notes
objectcontext now readsobject & { user: number }instead of{} & { user: number }. The two behave the same, but three exact-type assertions changed (two in@orpc/effect).Testing
context.test-d.tsand a.use()→.handler()narrowing case inbuilder.test-d.ts. All fail on the old code and pass with the fix.tscover all type tests andtype:checkin all 28 packages pass, plus eslint.