Skip to content

RpcStub<T> doesn't preserve object return types - only primitives work #112

@PizzaConsole

Description

@PizzaConsole

Description

When using newWebSocketRpcSession or newHttpBatchRpcSession with a TypeScript generic parameter, the RpcStub<T> type preserves primitive return types (string, number, boolean) but fails to preserve object/interface return types, returning any instead.

Minimal Reproducible Example

import { newWebSocketRpcSession, type RpcStub } from 'capnweb';

// Define types
type ApiMessage = { message: string; timestamp: string };

// Define API interface
interface MyApi {
  getPrimitive(): Promise<string>;           // Returns primitive
  getObject(): Promise<ApiMessage>;          // Returns object
  hello(name: string): Promise<ApiMessage>;  // Returns object
}

// Create client
const apiStub = newWebSocketRpcSession<MyApi>('wss://example.com/api');

// Primitives work correctly ✅
const primitive = await apiStub.getPrimitive();
//    ^? Type is 'string' ✅

// Objects return 'any' ❌
const object = await apiStub.getObject();
//    ^? Type is 'any' ❌

const helloResult = await apiStub.hello('world');
//    ^? Type is 'any' ❌

Expected Behavior

All method return types should be preserved, regardless of whether they're primitives or objects:

const primitive = await apiStub.getPrimitive();
//    ^? string ✅

const object = await apiStub.getObject();
//    ^? Should be: ApiMessage

const helloResult = await apiStub.hello('world');
//    ^? Should be: ApiMessage

Actual Behavior

Only primitive return types are preserved. Object/interface return types become any:

const apiStub: RpcStub<MyApi> = newWebSocketRpcSession<MyApi>('wss://example.com/api');
//    ^? Correctly shows: RpcStub<MyApi>

const primitive = await apiStub.getPrimitive();
//    ^? string ✅ Works!

const object = await apiStub.getObject();
//    ^? any ❌ Should be ApiMessage

const helloResult = await apiStub.hello('world');
//    ^? any ❌ Should be ApiMessage

// No type checking for objects
console.log(helloResult.anythingGoes); // No error

Root Cause

The RpcStub<T> type definition appears to handle primitives correctly but wraps object return types in complex proxy types (for promise pipelining support) that resolve to any instead of the original type.

Workaround

Manually cast each result:

const helloResult = await apiStub.hello('world') as ApiMessage;
//    ^? Now correctly typed as: ApiMessage ✅

Environment

  • capnweb version: 0.2.0
  • TypeScript version: 5.8.3
  • Node version: 22.15.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions