Skip to content

perf(contract, server): infer schema types by indexed access - #2451

Open
dinwwwh wants to merge 2 commits into
mainfrom
claude/perf-schema-infer-indexed-access
Open

dinwwwh wants to merge 2 commits into
mainfrom
claude/perf-schema-infer-indexed-access

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Summary

InferSchemaInput and InferSchemaOutput now read ~standard.types by indexed access, the same way the Standard Schema spec defines its own InferInput / InferOutput, instead of matching T extends StandardSchemaV1<infer U, any>.

Zod, Valibot, ArkType and others each ship their own copy of the Standard Schema interface. With the conditional form, TypeScript had to compare that whole interface structurally for every schema. Indexed access skips that comparison.

Measurements

Measured with the typecheck project of orpc-benchmarks (3,000 procedures plus a typed client call for each), tsc --singleThreaded --extendedDiagnostics, TypeScript 7.0.2. "Before" is the published 2.0.0-beta.50, "after" is @orpc/contract and @orpc/server built from this branch. These counts are deterministic.

Inputs declared with Instantiations Types
The benchmark's vendored Standard Schema interface 1,040,001 → 912,444 (−12.3%) 247,841 → 204,327 (−17.6%)
Zod 4.6.7 1,982,622 → 1,750,047 (−11.7%) 365,229 → 321,714 (−11.9%)
Valibot 1.5.0 1,674,556 → 1,381,969 (−17.5%) 335,747 → 292,233 (−13.0%)
oRPC's own Schema<I, O> type 886,914 → 894,428 (+0.8%) 169,788 → 189,311 (+11.5%)

The last row gets slightly worse. When a schema's type is the very same declaration oRPC imports, the conditional form had a fast path that indexed access does not beat.

This is independent of #2452. With both applied, the benchmark goes from 1,040,001 to 783,432 instantiations (−24.7%).

Behavior

  • any schema: still infers unknown, through the 0 extends 1 & T guard. Without the guard, 24 type-test assertions fail. The cheaper unknown extends T guard breaks two ai-sdk type tests, so it is not used.
  • createProcedureClient: two internal casts changed. They relied on the old conditional resolving to any inside generic code. No runtime change.

Schemas from Zod, Valibot, ArkType and anything typed as Schema<I, O> are unaffected. The difference is for a hand-written schema whose type does not declare usable types:

Schema type Before After
types omitted input unknown, output read from validate's return type input unknown, output unknown
types: undefined declared explicitly input unknown, output read from validate's return type input never, output never

Both can be avoided at a cost, and neither option is in this PR:

  • Returning unknown for the types: undefined case too, with [T['~standard']['types']] extends [undefined] as the guard: 931,964 instantiations on the benchmark (−10.4% instead of −12.3%).
  • Keeping the old conditional as a fallback when types is omitted: 949,954 (−8.7%).

Verification

  • pnpm check, pnpm test (4,585 tests) and pnpm docs:validate pass.
  • schema.test-d.ts gains assertions for Valibot, ArkType, a union of schemas, AnySchema, any, and a schema with types omitted. All but the last also pass on main; the last one is the difference described above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VVaw7SLCp5Vav859sAFS6N

InferSchemaInput and InferSchemaOutput matched every schema against
StandardSchemaV1<infer U, any>. Schema libraries ship their own copy of
that interface, so TypeScript compared the whole interface structurally
for each schema. Reading `~standard.types` by indexed access, as the
spec's own InferInput/InferOutput do, avoids that work.

An `any` schema still infers `unknown`. Two internal casts in
createProcedureClient relied on the old conditional resolving to `any`
inside generic code.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SLCp5Vav859sAFS6N
@pkg-pr-new

pkg-pr-new Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

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

@orpc/arktype

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

@orpc/bun

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

@orpc/client

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

@orpc/cloudflare

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

@orpc/contract

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

@orpc/effect

npm i https://pkg.pr.new/@orpc/effect@2451

@orpc/evlog

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

@orpc/hibernation

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

@orpc/json-schema

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

@orpc/experimental-lock

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

@orpc/mcp

npm i https://pkg.pr.new/@orpc/mcp@2451

@orpc/msw

npm i https://pkg.pr.new/@orpc/msw@2451

@orpc/nest

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

@orpc/next

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

@orpc/node

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

@orpc/openapi

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

@orpc/opentelemetry

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

@orpc/pinia-colada

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

@orpc/pino

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

@orpc/publisher

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

@orpc/ratelimit

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

@orpc/server

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

@orpc/shared

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

@orpc/swr

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

@orpc/tanstack-query

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

@orpc/trpc

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

@orpc/valibot

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

@orpc/zod

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

commit: 78eb930

@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Summary

Summary by CodeRabbit

  • TypeScript Improvements
    • Improved input and output type inference for Valibot and ArkType schemas, schema unions, and other supported schemas.
    • Inference checks distinguish AnySchema (which resolves to any) from any and untyped schemas (which resolve to unknown).
    • Existing Zod type inference remains supported.

Schema input and output inference now reads Standard Schema metadata and returns unknown for any and an untyped schema. Procedure-client tracing changes its async-iterator and ReadableStream return casts.

Changes: Schema type inference

Layer / File(s) Summary
Schema inference contract
packages/contract/src/schema.ts, packages/contract/src/schema.test-d.ts
InferSchemaInput and InferSchemaOutput read types from the non-nullable ~standard.types property and return unknown for any. Type assertions cover Valibot, ArkType, schema unions, AnySchema, any, and an untyped schema.

Procedure client stream typing

Layer / File(s) Summary
Tracing output casts
packages/server/src/procedure-client.ts
The async-iterator and ReadableStream branches cast traced results to any. The ReadableStream branch passes a ReadableStream<unknown> local to traceReadableStream.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Possibly related PRs

  • middleapi/orpc#216: It uses InferSchemaOutput and edits createProcedureClient, which this PR also changes.
Merge Risk: Low · up to 78eb9

Custom schemas that do not declare Standard Schema types metadata will now infer unknown for their input and output types. Zod, Valibot and ArkType declare it and are unaffected. This is an intentional, tested tradeoff made for type-check performance, so maintainers should confirm they accept it, but it should not block the merge.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title "perf(contract, server): infer schema types by indexed access" matches the main change. The PR switches InferSchemaInput and InferSchemaOutput from conditional matching to indexed access on …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description explains the change to InferSchemaInput and InferSchemaOutput, which now read ~standard.types by indexed access instead of matching StandardSchemaV1 generics. It gives benchmark measur…
Full details: Title check

Explanation

The title "perf(contract, server): infer schema types by indexed access" matches the main change. The PR switches InferSchemaInput and InferSchemaOutput from conditional matching to indexed access on ~standard.types. The title also names the two packages touched (contract and server). The server change is small: two internal casts in createProcedureClient. The title is specific and uses a conventional perf prefix.

Full details: Description check

Explanation

The description explains the change to InferSchemaInput and InferSchemaOutput, which now read ~standard.types by indexed access instead of matching StandardSchemaV1 generics. It gives benchmark measurements, lists the behavior changes for any schemas and schemas without declared types, and describes the two internal casts in createProcedureClient. It also lists verification steps and the new schema.test-d.ts assertions. This matches the changeset in schema.ts, schema.test-d.ts, and procedure-client.ts, so the check passes.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3aca8bf2-013d-419a-b441-158f43639886
Commits

Reviewing files that changed from the base of the PR and between 21e85be and 78eb930.

Files selected for processing: 2

  • packages/contract/src/schema.test-d.ts
  • packages/contract/src/schema.ts
Files with no reviewable changes: 1

  • packages/contract/src/schema.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.

@codecov

codecov Bot commented Nov 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
orpc 78eb930 Commit Preview URL

Branch Preview URL
Nov 12 2026, 09:26 AM

@codspeed

codspeed Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 62.95%

2 warnings

⚠️ 5 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ 1 benchmark with a significant change ran on different hardware

Its base and head results were measured on machines with a different CPU, core count or memory, so the change might not come from the code. Run base and head on the same runner type.

Learn more about Macro Runners

See affected benchmarks

⚡ 1 improved benchmark
✅ 27 untouched benchmarks
⏩ 2 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ not modified (304) 1,078.8 µs 662.1 µs +62.95%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/perf-schema-infer-indexed-access (78eb930) with main (da7ad7b)2

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (9ad3104) during the generation of this report, so da7ad7b was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Additional comments: 1

packages/contract/src/schema.ts-40-40: 1

40-40: Major · Functional Correctness · Quick win

Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Preserve output inference for schemas without declared types.

Standard Schema v1.1.0 permits schemas to omit types. (raw.githubusercontent.com) For a schema whose validate returns { value: 123 }, but which does not declare types, this change returns unknown instead of the previously inferred number.

ProcedureClient consumes this result in packages/server/src/procedure-client.ts, Line 48. Previously valid operations such as calling .toFixed() on the resolved output now fail type checking.

Keep indexed access for schemas that declare types. Restore conditional inference as a fallback for other schemas, and add a regression assertion for this case.

Review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4b42bf8e-e3df-460a-8088-5df41c3ab1ca
Commits

Reviewing files that changed from the base of the PR and between 9ad3104 and 21e85be.

Files selected for processing: 3

  • packages/contract/src/schema.test-d.ts
  • packages/contract/src/schema.ts
  • packages/server/src/procedure-client.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread packages/contract/src/schema.ts Resolved
Replaces the comment on InferSchemaInput and InferSchemaOutput with a
test of the case it described.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SLCp5Vav859sAFS6N

@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 critical issues — two rough edges worth a look.

Reviewed changes

  • InferSchemaInput / InferSchemaOutput now use indexed access — schema.ts replaces the structural T extends StandardSchemaV1<infer U, any> conditional with NonNullable<T['~standard']['types']>['input' | 'output'], keeping the 0 extends 1 & T guard so any schemas still infer unknown.
  • procedure-client.ts casts widened — as typeof output became as any in the async-iterator and ReadableStream branches, with a ReadableStream<unknown> intermediate for the stream branch. No runtime change.
  • Type tests extended — valibot, arktype, a union of schemas, AnySchema and any are now pinned in schema.test-d.ts.

I re-checked the parts that were easiest to get wrong. Restoring as typeof output fails with TS2352/TS2345, so the widened casts are genuinely required now that InferSchemaOutput<TOutputSchema> stays deferred inside createProcedureClient. pnpm check (oxlint with typeCheck) passes across 937 files, and every library that vendors the spec declares types (@standard-schema/spec had it from 1.0.0; verified zod 3.25.76, zod 4.x, valibot 1.5, effect 4.1), so the schema-without-types case is limited to hand-written schemas.

ℹ️ An explicit types: undefined infers never, not unknown

The PR description covers schemas that omit types (now unknown), but the expression has a second outcome: a schema type that declares types: undefined explicitly resolves NonNullable<T['~standard']['types']> to never, so InferSchemaOutput is never and any procedure or client built on it can never produce a value. Neither shape is asserted in schema.test-d.ts, so the intentional tradeoff has no regression pin.

Technical details
# `NonNullable<T['~standard']['types']>` has two edge outcomes

## Affected sites
- `packages/contract/src/schema.ts:29-31` — `InferSchemaInput`
- `packages/contract/src/schema.ts:38-40` — `InferSchemaOutput`

## Repro (verified with tsc 7.0.2 against 21e85be)
```ts
declare const omitted: {
  '~standard': {
    version: 1
    vendor: 'x'
    validate: (value: unknown) => { value: { a: number } }
  }
}
// InferSchemaOutput<typeof omitted> === unknown   — resolved through the AnySchema constraint

declare const explicitUndefined: {
  '~standard': {
    version: 1
    vendor: 'x'
    validate: (value: unknown) => { value: { a: number } }
    types: undefined
  }
}
// InferSchemaOutput<typeof explicitUndefined> === never
```

## Required outcome
- Decide the intended result for both shapes and pin each with `expectTypeOf` in `schema.test-d.ts`.
- Consider a sentence in `apps/content/docs/integrations/standard-schema.mdx` telling custom-schema authors that `types` is what oRPC reads for inference.

## Open questions for the human
- Is `never` acceptable for the explicit-`undefined` shape, or should it fall back to `unknown` (e.g. a conditional on `NonNullable<...> extends never`)?

ℹ️ MergedSchema still pays the same structural comparison on the builder path

MergedSchema (line 42) is the last T extends Schema<infer ...> comparison in this file, and every .input()/.output() call instantiates it, so it is plausible that some of the remaining benchmark instantiations come from there. It did not need to change for this PR, but a measurement would tell whether it is worth the same treatment — note that an indexed-access version would no longer distribute over a union of schema types the way the current conditional does.

Technical details
# Remaining structural comparison in `schema.ts`

## Affected sites
- `packages/contract/src/schema.ts:42-46` — `MergedSchema` matches both operands structurally
- `packages/contract/src/builder-variants.ts:43,99` — instantiated per `.input()` / `.output()` call

## Required outcome
- Either confirm `MergedSchema` is not a meaningful share of the remaining instantiations, or extend the same optimisation with a measurement.

## Suggested approach (optional)
- `Schema<InferSchemaInput<T> & InferSchemaInput<U>, InferSchemaOutput<T> & InferSchemaOutput<U>>` is the indexed-access analogue, but it collapses a union `T` into a single schema instead of distributing — confirm that difference is acceptable before switching.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@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.

ℹ️ One small note on the new commit.

Reviewed changes

  • 78eb930 adds untypedSchema assertions — the omitted-types case is now pinned to unknown for both directions in schema.test-d.ts, which documents the tradeoff the PR description makes.
  • 78eb930 removes the explanatory comment that sat above the two aliases, replacing it with that test.

The delta has no behavior change: oxlint --type-check on schema.ts and schema.test-d.ts reports 0 errors at 78eb930, and the new assertions describe exactly the unknown outcome described in the PR body.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Comment thread packages/contract/src/schema.ts Resolved

This branch has not been deployed

No deployments
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.

2 participants