Skip to content

fix(cloudflare/workers): type named entrypoint bindings with their RPC surface - #1415

Draft
Mkassabov wants to merge 1 commit into
mainfrom
fix/worker-entrypoint-rpc-types
Draft

fix(cloudflare/workers): type named entrypoint bindings with their RPC surface#1415
Mkassabov wants to merge 1 commit into
mainfrom
fix/worker-entrypoint-rpc-types

Conversation

@Mkassabov

Copy link
Copy Markdown
Contributor

Cloudflare.WorkerEntrypoint(target, "Api") returned a non-generic WorkerEntrypointBinding, so no information about the entrypoint class reached InferEnv. Its first branch mapped the binding to a bare Fetcher (i.e. Fetcher<undefined>fetch + connect only), so calling an RPC method on it failed to compile:

error TS2339: Property 'greet' does not exist on type
  '{ fetch(...): Promise<Response>; connect(...): Socket; }'

The entrypoint name is a runtime string, so nothing links "Api" to the target module's exports on its own. The class is now passed as a type argument:

+import type { Api } from "./target/src/worker.ts";

 env: {
-  API: Cloudflare.WorkerEntrypoint(target, "Api"),
+  API: Cloudflare.WorkerEntrypoint<typeof Api>(target, "Api"),
 },
// caller worker
const greeting = await env.API.greet("alice"); // Promise<string>

WorkerEntrypointBinding carries the class in a phantom field, and InferEnv maps it through a new EntrypointStub:

  • class extending cloudflare:workers' WorkerEntrypoint (branded) → Service<Instance>
  • unbranded method bag → Fetcher & { …methods }
  • alchemy Effect-native Rpc<Shape> worker → RpcWireShape<Shape> & Service
  • type argument omitted → the target Worker's own default-entrypoint type, so existing call sites keep their current Fetcher and still compile

test/types/WorkerEntrypointEnv.ts is a compile-only probe: greet is Promise<string>, non-promise returns are promisified, fetch still resolves, and the untyped form rejects greet under @ts-expect-error.

The docs claimed the old behavior worked ("InferEnv types the binding as a Fetcher service stub — RPC methods are called on it directly"); the JSDoc and the Workers guide now teach the type-argument form.

@alchemy-version-bot

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

Alchemy

alchemy

bun add https://pkg.ing/alchemy/9c0075a

@alchemy.run/better-auth

bun add https://pkg.ing/@alchemy.run/better-auth/9c0075a

@alchemy.run/cloudflare-runtime

bun add https://pkg.ing/@alchemy.run/cloudflare-runtime/9c0075a

@alchemy.run/frontend-frameworks

bun add https://pkg.ing/@alchemy.run/frontend-frameworks/9c0075a

@alchemy.run/node-utils

bun add https://pkg.ing/@alchemy.run/node-utils/9c0075a

@alchemy.run/pr-package

bun add https://pkg.ing/@alchemy.run/pr-package/9c0075a

@alchemy.run/floci

bun add https://pkg.ing/@alchemy.run/floci/9c0075a

Distilled

@distilled.cloud/core

bun add https://pkg.ing/@distilled.cloud/core/cb11c5b

@distilled.cloud/aws

bun add https://pkg.ing/@distilled.cloud/aws/cb11c5b

@distilled.cloud/axiom

bun add https://pkg.ing/@distilled.cloud/axiom/cb11c5b

@distilled.cloud/cloudflare

bun add https://pkg.ing/@distilled.cloud/cloudflare/cb11c5b

@distilled.cloud/hetzner

bun add https://pkg.ing/@distilled.cloud/hetzner/cb11c5b

@distilled.cloud/neon

bun add https://pkg.ing/@distilled.cloud/neon/cb11c5b

@distilled.cloud/planetscale

bun add https://pkg.ing/@distilled.cloud/planetscale/cb11c5b

export const Worker = Cloudflare.Worker("EntrypointEnvTypeProbe", {
script: "export default {}",
env: {
API: Cloudflare.WorkerEntrypoint<typeof Api>(target, "Api"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not Cloudflare.WorkerEntrypoint<Api>(target, "Api")

Comment on lines +156 to +185
/**
* Runtime type of a `Cloudflare.WorkerEntrypoint(...)` env entry.
*
* `Entrypoint` is the class the binding names, supplied as an explicit type
* argument (`Cloudflare.WorkerEntrypoint<typeof Api>(target, "Api")`) — the
* entrypoint name is a string at the value level, so nothing links it to the
* target module's exports on its own. With it, the entry types as that
* class's RPC surface (`Service<typeof Api>`); without it, the binding falls
* back to the target Worker's own default-entrypoint type: an Effect-native
* Worker's `Rpc<Shape>` wire shape, otherwise a bare `Fetcher`.
*/
export type EntrypointStub<Entrypoint, Target> = [Entrypoint] extends [
undefined,
]
? Target extends AlchemyRpc<infer Shape extends object>
? RpcWireShape<Shape> & Service
: Fetcher
: Entrypoint extends AlchemyRpc<infer Shape extends object>
? RpcWireShape<Shape> & Service
: Entrypoint extends abstract new (...args: any[]) => infer Instance
? EntrypointStubOf<Instance>
: EntrypointStubOf<Entrypoint>;

/** {@link EntrypointStub} for an entrypoint *instance* type. */
type EntrypointStubOf<Instance> = [Instance] extends [
Rpc.WorkerEntrypointBranded,
]
? Service<Extract<Instance, Rpc.WorkerEntrypointBranded>>
: // A plain method-bag shape (no `cloudflare:workers` brand).
Fetcher & { [K in keyof Instance]: Instance[K] };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we not already have something for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants