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
When using the ingress client (connect() from @restatedev/restate-sdk-clients) to create a workflow client, the generic type parameter D in workflowClient<D>() is never correctly inferred, regardless of how it's called. The result is always IngressWorkflowClient<unknown>, making workflowSubmit, workflowAttach, and workflowOutput resolve to never.
Root Cause
WorkflowDefinition<P, M> in @restatedev/restate-sdk-core is defined as:
TypeScript must infer D from WorkflowDefinitionFrom<D>, which is:
typeWorkflowDefinitionFrom<M>=MextendsWorkflowDefinition<string,unknown> ? M : WorkflowDefinition<string,M>;
The inference fails because WorkflowDefinition<P, M> is structurally just { name: P }. TypeScript can only infer D = { name: "MyWorkflow" } from the structure, losing the phantom M. Then Workflow<D> resolves to unknown, and all the conditional types in IngressWorkflowClient that check M extends Record<string, unknown> and M["run"] extends (...) collapse to never.
Reproduction
import*asrestatefrom"@restatedev/restate-sdk";import{connect}from"@restatedev/restate-sdk-clients";typeTopUpInput={userId: number;txHash: string};constmyWorkflow=restate.workflow({name: "MyWorkflow",handlers: {run: async(ctx: restate.WorkflowContext,input: TopUpInput)=>{return{status: "done"asconst};},},});constclient=connect({url: "http://localhost:8080"});// ❌ All of these produce IngressWorkflowClient<unknown>:// Attempt 1: Pass object directly (inference fails)constwf1=client.workflowClient(myWorkflow,"key1");// ^? IngressWorkflowClient<unknown>// Attempt 2: Explicit generic with typeof (same result)constwf2=client.workflowClient<typeofmyWorkflow>(myWorkflow,"key1");// ^? IngressWorkflowClient<unknown>// Attempt 3: Export type alias (same result)typeMyWorkflow=typeofmyWorkflow;constwf3=client.workflowClient<MyWorkflow>(myWorkflow,"key1");// ^? IngressWorkflowClient<unknown>// All of these fail:awaitwf1.workflowSubmit({userId: 1,txHash: "abc"});// ^^^^^^^^^^^^^^ — Type 'never' has no call signatures
Note
The internal SDK client (ctx.workflowClient<typeof myWorkflow>(...) inside a Restate handler) uses the same WorkflowDefinitionFrom<D> type but is documented to work with the typeof pattern. The issue may be specific to how the ingress client's .d.cts entry point resolves cross-package types, or may be a general phantom type inference bug in the SDK's type definitions.
With "moduleResolution": "node", TypeScript follows the "types" field → .d.cts files, while runtime imports follow ESM. This CJS/ESM type mismatch may contribute to the issue if TypeScript resolves @restatedev/restate-sdk-core's WorkflowDefinition through different module paths for the SDK vs the clients package.
Suggested Fix
The phantom type WorkflowDefinition<P, M> = { name: P } cannot carry M structurally. Two potential fixes:
Option A: Add a branded phantom field
declareconst__handlers: unique symbol;exporttypeWorkflowDefinition<Pextendsstring,M>={name: P;[__handlers]?: M;// phantom brand — never set at runtime};
This preserves M structurally so TypeScript can infer it from the argument.
Option B: Change workflowClient to accept the definition object directly
Instead of using WorkflowDefinitionFrom<D> (conditional type inference), accept WorkflowDefinition<string, D> directly:
Bug: workflowClient() loses generic type —
IngressWorkflowClient<unknown>and workflowSubmit isneverPackage
@restatedev/restate-sdk-clients@1.11.1(also affects@restatedev/restate-sdk-core@1.11.1)Description
When using the ingress client (connect() from @restatedev/restate-sdk-clients) to create a workflow client, the generic type parameter
DinworkflowClient<D>()is never correctly inferred, regardless of how it's called. The result is alwaysIngressWorkflowClient<unknown>, making workflowSubmit, workflowAttach, and workflowOutput resolve tonever.Root Cause
WorkflowDefinition<P, M>in @restatedev/restate-sdk-core is defined as:The handler map
Mis a phantom type parameter — it exists in the type signature but has no structural representation (the object is just{ name: P }).The workflow() function returns
WorkflowDefinition<P, M>and TypeScript preserves the phantomMin the declared type of the variable:However, when this value is passed to the ingress client's
workflowClient<D>():TypeScript must infer
DfromWorkflowDefinitionFrom<D>, which is:The inference fails because
WorkflowDefinition<P, M>is structurally just{ name: P }. TypeScript can only inferD = { name: "MyWorkflow" }from the structure, losing the phantomM. ThenWorkflow<D>resolves tounknown, and all the conditional types in IngressWorkflowClient that checkM extends Record<string, unknown>andM["run"] extends (...)collapse tonever.Reproduction
Note
The internal SDK client (
ctx.workflowClient<typeof myWorkflow>(...)inside a Restate handler) uses the sameWorkflowDefinitionFrom<D>type but is documented to work with thetypeofpattern. The issue may be specific to how the ingress client's .d.cts entry point resolves cross-package types, or may be a general phantom type inference bug in the SDK's type definitions.Environment
bun-types(Bun runtime)"moduleResolution": "node","module": "ES2022","strict": trueAdditional Context
The published package.json for both restate-sdk and restate-sdk-clients sets:
{ "types": "./dist/index.d.cts", "exports": { ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" } } }With
"moduleResolution": "node", TypeScript follows the"types"field → .d.cts files, while runtime imports follow ESM. This CJS/ESM type mismatch may contribute to the issue if TypeScript resolves @restatedev/restate-sdk-core'sWorkflowDefinitionthrough different module paths for the SDK vs the clients package.Suggested Fix
The phantom type
WorkflowDefinition<P, M> = { name: P }cannot carryMstructurally. Two potential fixes:Option A: Add a branded phantom field
This preserves
Mstructurally so TypeScript can infer it from the argument.Option B: Change workflowClient to accept the definition object directly
Instead of using
WorkflowDefinitionFrom<D>(conditional type inference), acceptWorkflowDefinition<string, D>directly:This avoids the lossy conditional type and lets TypeScript infer
Ddirectly as the handler map.Workaround
Use raw HTTP
fetch()calls to the Restate ingress instead of the typed SDK client: