Skip to content

fix(server): keep union and index-signature context types across .use() - #2110

Merged
dinwwwh merged 2 commits into
middleapi:mainfrom
dinwwwh:claude/union-context-narrowing-use-50db0b
Sep 28, 2026
Merged

dinwwwh merged 2 commits into
middleapi:mainfrom
dinwwwh:claude/union-context-narrowing-use-50db0b

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 28, 2026

Copy link
Copy Markdown
Member

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' }, so if (context.kind === 'a') context.a was a tsc error. With { [k: string]: unknown, db: string }, context.db became unknown. The handler now gets the full union, which narrows as expected, and db stays string.

Fixes

  • Discriminated union contexts keep narrowing after .use() adds context.
  • Named keys next to an index signature keep their types after .use().
  • A later .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.
  • Optional and readonly markers on context keys are kept.

Performance

  • No type-check cost over the old 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

  • Merging into a bare object context now reads object & { user: number } instead of {} & { user: number }. The two behave the same, but three exact-type assertions changed (two in @orpc/effect).

Testing

  • New type tests for unions and index signatures in context.test-d.ts and a .use() → .handler() narrowing case in builder.test-d.ts. All fail on the old code and pass with the fix.
  • Root tsc over all type tests and type:check in all 28 packages pass, plus eslint.

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

codecov Bot commented Sep 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pkg-pr-new

pkg-pr-new Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@2110

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@2110

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@2110

@orpc/client

npm i https://pkg.pr.new/@orpc/client@2110

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@2110

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@2110

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@2110

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@2110

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@2110

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@2110

@orpc/experimental-lock

npm i https://pkg.pr.new/@orpc/experimental-lock@2110

@orpc/experimental-msw

npm i https://pkg.pr.new/@orpc/experimental-msw@2110

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@2110

@orpc/next

npm i https://pkg.pr.new/@orpc/next@2110

@orpc/node

npm i https://pkg.pr.new/@orpc/node@2110

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@2110

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@2110

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@2110

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@2110

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@2110

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@2110

@orpc/server

npm i https://pkg.pr.new/@orpc/server@2110

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@2110

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@2110

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@2110

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@2110

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@2110

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@2110

commit: 0553669

@codspeed

codspeed Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing dinwwwh:claude/union-context-narrowing-use-50db0b (0553669) with main (1d40246)

Open in CodSpeed

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Minor suggestion only — one edge case in the new distribution wrapper.

Reviewed changes

  • Type-level context merge fix — replaced Omit with a homomorphic OmitKeys in both MergedContext and MergedInitialContext, so discriminated unions and named keys beside index signatures survive .use().
  • Union distribution — MergedInitialContext now distributes over a union TInContext (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.ts and an end-to-end .use() → .handler() narrowing test in builder.test-d.ts; updated three exact-type assertions that shifted from {} to object for bare-object contexts.

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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread packages/server/src/context.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Since the prior pullfrog review at c0b20ccd, the only new commit (0553669b) is a refactor:

  • Extracted DistributiveOmit to @orpc/shared — the private OmitKeys helper in server/context.ts is now the public DistributiveOmit<T, K extends PropertyKey> in shared/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.ts to the shared helper — MergedContext and MergedInitialContext now import it; the resolved types are unchanged from c0b20ccd.

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.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit eab53d6 into middleapi:main Sep 28, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant