From 9bae12098e3db63ff921d650bc352205106d6b31 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 09:20:49 +0000 Subject: [PATCH 1/3] perf(server): make RouterClient cheaper to type-check RouterClient inferred every key through the Procedure class and the Lazyable union. It now matches a procedure through its `~orpc` definition, unwraps Lazy with its own conditional and maps the remaining keys directly. The mapped type uses `as K` so TypeScript does not infer the router backwards from an expected client, which is unbounded for a recursive type; a test covers it. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01VVaw7SLCp5Vav859sAFS6N --- packages/server/src/router-client.test-d.ts | 41 +++++++++++++++++++++ packages/server/src/router-client.ts | 21 +++++++---- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/packages/server/src/router-client.test-d.ts b/packages/server/src/router-client.test-d.ts index 8895dab3d..5f5dd314e 100644 --- a/packages/server/src/router-client.test-d.ts +++ b/packages/server/src/router-client.test-d.ts @@ -1,9 +1,12 @@ +import type { AnyNestedClient } from '@orpc/client' import type { Schema } from '@orpc/contract' import * as z from 'zod' import { os } from './builder' +import type { Lazyable } from './lazy' import type { ProcedureClient } from './procedure-client' import type { RouterClient } from './router-client' +import { createRouterClient } from './router-client' const errorMap = { BASE: { @@ -58,6 +61,38 @@ describe('RouterClient', () => { >() }) + it('unwraps lazy at any depth', () => { + const lazyRouter = { + ping: os.lazy(() => Promise.resolve({ default: router.ping })), + nested: os.lazy(() => Promise.resolve({ default: { lazy: router.lazy } })), + } + const client = {} as RouterClient + + expectTypeOf(client.ping).toEqualTypeOf< + ProcedureClient + >() + + expectTypeOf(client.nested.lazy.peng).toEqualTypeOf< + ProcedureClient, object> + >() + }) + + it('supports a key that is either lazy or not', () => { + type LazyableRouter = { + ping: Lazyable + nested: Lazyable + } + const client = {} as RouterClient + + expectTypeOf(client.ping).toEqualTypeOf< + ProcedureClient + >() + + expectTypeOf(client.nested.pong).toEqualTypeOf< + ProcedureClient + >() + }) + it('support single procedure', () => { type P = RouterClient<(typeof router)['ping'], { cache?: boolean }> @@ -66,3 +101,9 @@ describe('RouterClient', () => { >() }) }) + +describe('createRouterClient', () => { + it('accepts an untyped router', () => { + const _client: AnyNestedClient = createRouterClient({} as any, {}) + }) +}) diff --git a/packages/server/src/router-client.ts b/packages/server/src/router-client.ts index 11325f21a..2895c98cd 100644 --- a/packages/server/src/router-client.ts +++ b/packages/server/src/router-client.ts @@ -6,6 +6,7 @@ import { resolveMaybeOptionalOptions, toArray } from '@orpc/shared' import type { Lazyable } from './lazy' import { Lazy } from './lazy' +import type { ProcedureDefinition } from './procedure' import { Procedure } from './procedure' import type { ProcedureClient, ProcedureClientOptions } from './procedure-client' import { createProcedureClient } from './procedure-client' @@ -19,14 +20,18 @@ import { getRouter } from './router-utils' * * @see {@link https://orpc.dev/docs/client/client-side#creating-a-client | Client-Side Clients - Creating a Client} */ -export type RouterClient = - TRouter extends Procedure - ? ProcedureClient - : { - [K in keyof TRouter]: TRouter[K] extends Lazyable - ? RouterClient - : never - } +export type RouterClient< + TRouter extends AnyRouter, + TClientContext extends ClientContext = object, +> = RouterClientInner + +export type RouterClientInner = T extends { + '~orpc': ProcedureDefinition +} + ? ProcedureClient + : T extends Lazy + ? RouterClientInner + : { [K in keyof T as K]: RouterClientInner } /** * Creates a server-side client that calls the router's procedures directly, From dea4b4d8a1fe399da13d9b331fd74722a451cab2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 09:28:56 +0000 Subject: [PATCH 2/3] refactor(server): keep the RouterClient helper type local A project that emits declarations writes out the expanded client shape, never the helper's name, so the helper does not need to be exported. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01VVaw7SLCp5Vav859sAFS6N --- packages/server/src/router-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/router-client.ts b/packages/server/src/router-client.ts index 2895c98cd..ad648eb53 100644 --- a/packages/server/src/router-client.ts +++ b/packages/server/src/router-client.ts @@ -25,7 +25,7 @@ export type RouterClient< TClientContext extends ClientContext = object, > = RouterClientInner -export type RouterClientInner = T extends { +type RouterClientInner = T extends { '~orpc': ProcedureDefinition } ? ProcedureClient From 5f088a2f789dc4c9654981c8c0a2261a840a7ccf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 09:41:20 +0000 Subject: [PATCH 3/3] fix(server): keep an optional router key as never in RouterClient Mapping every key directly turned an optional router key into an optional client. Inside a lazy router that client is always a truthy proxy at runtime, so the type promised a check that cannot work. Each key is now guarded with `extends Lazyable`, as before this branch, and a test pins it. The guard also stops TypeScript from inferring the router backwards through the mapped type, so the `as K` clause is no longer needed. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01VVaw7SLCp5Vav859sAFS6N --- packages/server/src/router-client.test-d.ts | 6 ++++++ packages/server/src/router-client.ts | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/server/src/router-client.test-d.ts b/packages/server/src/router-client.test-d.ts index 5f5dd314e..e6ec1aea7 100644 --- a/packages/server/src/router-client.test-d.ts +++ b/packages/server/src/router-client.test-d.ts @@ -93,6 +93,12 @@ describe('RouterClient', () => { >() }) + it('maps an optional key to never', () => { + type OptionalRouter = { ping?: typeof router.ping } + + expectTypeOf>().toEqualTypeOf<{ ping?: never }>() + }) + it('support single procedure', () => { type P = RouterClient<(typeof router)['ping'], { cache?: boolean }> diff --git a/packages/server/src/router-client.ts b/packages/server/src/router-client.ts index ad648eb53..debd93530 100644 --- a/packages/server/src/router-client.ts +++ b/packages/server/src/router-client.ts @@ -31,7 +31,11 @@ type RouterClientInner = T extends { ? ProcedureClient : T extends Lazy ? RouterClientInner - : { [K in keyof T as K]: RouterClientInner } + : { + [K in keyof T]: T[K] extends Lazyable + ? RouterClientInner + : never + } /** * Creates a server-side client that calls the router's procedures directly,