From 21e85be99752f947a2a9567e7f149156abde1a30 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 09:14:58 +0000 Subject: [PATCH 1/2] perf(contract, server): infer schema types by indexed access InferSchemaInput and InferSchemaOutput matched every schema against StandardSchemaV1. 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 Claude-Session: https://claude.ai/code/session_01VVaw7SLCp5Vav859sAFS6N --- packages/contract/src/schema.test-d.ts | 19 +++++++++++++++++++ packages/contract/src/schema.ts | 16 ++++++++++++---- packages/server/src/procedure-client.ts | 9 ++++----- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/contract/src/schema.test-d.ts b/packages/contract/src/schema.test-d.ts index 1703fd283..16f67a546 100644 --- a/packages/contract/src/schema.test-d.ts +++ b/packages/contract/src/schema.test-d.ts @@ -7,6 +7,15 @@ import type { AnySchema, InferSchemaInput, InferSchemaOutput, MergedSchema, Sche // Schemas should have distinct TInput and TOutput types to ensure correct inference. const inputSchema = z.object({ input: z.number().transform((n) => `${n}`) }) const outputSchema = z.object({ output: z.string().transform((s) => Number(s)) }) +const valibotSchema = v.object({ + value: v.pipe( + v.string(), + v.transform((s) => Number(s)), + ), +}) +const arktypeSchema = arktypeType({ value: 'string' }) + +type UnionSchema = Schema<{ a: number }, { a: string }> | Schema<{ b: number }, { b: string }> describe('Schema', () => { it('supports any standard schema', () => { @@ -28,11 +37,21 @@ describe('Schema', () => { it('InferSchemaInput', () => { expectTypeOf>().toEqualTypeOf<{ input: number }>() expectTypeOf>().toEqualTypeOf<{ output: string }>() + expectTypeOf>().toEqualTypeOf<{ value: string }>() + expectTypeOf>().toEqualTypeOf<{ value: string }>() + expectTypeOf>().toEqualTypeOf<{ a: number } | { b: number }>() + expectTypeOf>().toBeAny() + expectTypeOf>().toEqualTypeOf() }) it('InferSchemaOutput', () => { expectTypeOf>().toEqualTypeOf<{ input: string }>() expectTypeOf>().toEqualTypeOf<{ output: number }>() + expectTypeOf>().toEqualTypeOf<{ value: number }>() + expectTypeOf>().toEqualTypeOf<{ value: string }>() + expectTypeOf>().toEqualTypeOf<{ a: string } | { b: string }>() + expectTypeOf>().toBeAny() + expectTypeOf>().toEqualTypeOf() }) describe('MergedSchema', () => { diff --git a/packages/contract/src/schema.ts b/packages/contract/src/schema.ts index 846ff8ed4..8f245e2dc 100644 --- a/packages/contract/src/schema.ts +++ b/packages/contract/src/schema.ts @@ -15,21 +15,29 @@ export type AnySchema = Schema export type SchemaIssue = StandardSchemaV1.Issue +// `InferSchemaInput` and `InferSchemaOutput` read `types` by indexed access, as the spec's own +// `InferInput`/`InferOutput` do, instead of `T extends StandardSchemaV1`. +// Schema libraries ship their own copy of the interface, so the conditional compared the +// whole interface structurally for every schema. +// `0 extends 1 & T` keeps `unknown` for an `any` schema. + /** * Infers the input type of a schema. * * @see {@link https://orpc.dev/docs/metadata | Metadata} */ -export type InferSchemaInput = - T extends StandardSchemaV1 ? UInput : never +export type InferSchemaInput = 0 extends 1 & T + ? unknown + : NonNullable['input'] /** * Infers the output type of a schema. * * @see {@link https://orpc.dev/docs/metadata | Metadata} */ -export type InferSchemaOutput = - T extends StandardSchemaV1 ? UOutput : never +export type InferSchemaOutput = 0 extends 1 & T + ? unknown + : NonNullable['output'] export type MergedSchema = T extends Schema diff --git a/packages/server/src/procedure-client.ts b/packages/server/src/procedure-client.ts index 0f2f23a1c..d61ef45f4 100644 --- a/packages/server/src/procedure-client.ts +++ b/packages/server/src/procedure-client.ts @@ -154,16 +154,15 @@ export function createProcedureClient< : output, { mapError: reconcileError }, ), - ) as typeof output + ) as any } else if (getTracer() && (output as unknown) instanceof ReadableStream) { /** * @warning * Remember use `override` for ReadableStream to remain other special properties */ - return override( - output, - traceReadableStream('consume_octet_stream_output', output), - ) as typeof output + const stream = output as unknown as ReadableStream + + return override(stream, traceReadableStream('consume_octet_stream_output', stream)) as any } return output From 78eb930937e97839f073fa4094a4d68d16341fbf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 09:22:38 +0000 Subject: [PATCH 2/2] test(contract): cover a schema without declared types Replaces the comment on InferSchemaInput and InferSchemaOutput with a test of the case it described. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01VVaw7SLCp5Vav859sAFS6N --- packages/contract/src/schema.test-d.ts | 9 +++++++++ packages/contract/src/schema.ts | 6 ------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/contract/src/schema.test-d.ts b/packages/contract/src/schema.test-d.ts index 16f67a546..caf456e01 100644 --- a/packages/contract/src/schema.test-d.ts +++ b/packages/contract/src/schema.test-d.ts @@ -14,6 +14,13 @@ const valibotSchema = v.object({ ), }) const arktypeSchema = arktypeType({ value: 'string' }) +const untypedSchema = { + '~standard': { + version: 1 as const, + vendor: 'custom', + validate: (value: unknown) => ({ value: value as string }), + }, +} type UnionSchema = Schema<{ a: number }, { a: string }> | Schema<{ b: number }, { b: string }> @@ -42,6 +49,7 @@ it('InferSchemaInput', () => { expectTypeOf>().toEqualTypeOf<{ a: number } | { b: number }>() expectTypeOf>().toBeAny() expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() }) it('InferSchemaOutput', () => { @@ -52,6 +60,7 @@ it('InferSchemaOutput', () => { expectTypeOf>().toEqualTypeOf<{ a: string } | { b: string }>() expectTypeOf>().toBeAny() expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() }) describe('MergedSchema', () => { diff --git a/packages/contract/src/schema.ts b/packages/contract/src/schema.ts index 8f245e2dc..ec8783673 100644 --- a/packages/contract/src/schema.ts +++ b/packages/contract/src/schema.ts @@ -15,12 +15,6 @@ export type AnySchema = Schema export type SchemaIssue = StandardSchemaV1.Issue -// `InferSchemaInput` and `InferSchemaOutput` read `types` by indexed access, as the spec's own -// `InferInput`/`InferOutput` do, instead of `T extends StandardSchemaV1`. -// Schema libraries ship their own copy of the interface, so the conditional compared the -// whole interface structurally for every schema. -// `0 extends 1 & T` keeps `unknown` for an `any` schema. - /** * Infers the input type of a schema. *