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
{{ message }}
Repository navigation
perf(contract, server): infer schema types by indexed access - #2451
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.
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
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.
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.
The async-iterator and ReadableStream branches cast traced results to any. The ReadableStream branch passes a ReadableStream<unknown> local to traceReadableStream.
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: 4
Check name
Status
Explanation
Title check
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
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
Check skipped because no linked issues were found for this pull request.
Description check
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.
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.
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.
⚠️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.
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. ↩
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.
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
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 omittypes (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)```tsdeclareconst omitted: {
'~standard': {
version:1
vendor:'x'
validate: (value:unknown) => { value: { a:number } }
}
}
// InferSchemaOutput<typeof omitted> === unknown — resolved through the AnySchema constraintdeclareconst 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.
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.
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
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
InferSchemaInputandInferSchemaOutputnow read~standard.typesby indexed access, the same way the Standard Schema spec defines its ownInferInput/InferOutput, instead of matchingT 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
typecheckproject of orpc-benchmarks (3,000 procedures plus a typed client call for each),tsc --singleThreaded --extendedDiagnostics, TypeScript 7.0.2. "Before" is the published2.0.0-beta.50, "after" is@orpc/contractand@orpc/serverbuilt from this branch. These counts are deterministic.Schema<I, O>typeThe 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
anyschema: still infersunknown, through the0 extends 1 & Tguard. Without the guard, 24 type-test assertions fail. The cheaperunknown extends Tguard breaks twoai-sdktype tests, so it is not used.createProcedureClient: two internal casts changed. They relied on the old conditional resolving toanyinside 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 usabletypes:typesomittedunknown, output read fromvalidate's return typeunknown, outputunknowntypes: undefineddeclared explicitlyunknown, output read fromvalidate's return typenever, outputneverBoth can be avoided at a cost, and neither option is in this PR:
unknownfor thetypes: undefinedcase too, with[T['~standard']['types']] extends [undefined]as the guard: 931,964 instantiations on the benchmark (−10.4% instead of −12.3%).typesis omitted: 949,954 (−8.7%).Verification
pnpm check,pnpm test(4,585 tests) andpnpm docs:validatepass.schema.test-d.tsgains assertions for Valibot, ArkType, a union of schemas,AnySchema,any, and a schema withtypesomitted. All but the last also pass onmain; the last one is the difference described above.🤖 Generated with Claude Code
https://claude.ai/code/session_01VVaw7SLCp5Vav859sAFS6N