diff --git a/.changeset/olive-frogs-invent.md b/.changeset/olive-frogs-invent.md new file mode 100644 index 00000000..7504b375 --- /dev/null +++ b/.changeset/olive-frogs-invent.md @@ -0,0 +1,7 @@ +--- +"partyserver": patch +--- + +Allow user-defined interfaces as `Props`. + +Interfaces do not get implicit index signatures in TypeScript, so the previous `Record` bound rejected them with "Index signature for type 'string' is missing". `Props` is now bounded by `object` on `Server`, `getServerByName`, and `routePartykitRequest`, and the `T` constraints on the latter two are `Server` so subclasses declaring interface `Props` satisfy them too. Generic defaults stay `Record`, so untyped usage still reads props values as `unknown`. diff --git a/packages/partyserver/src/index.ts b/packages/partyserver/src/index.ts index bec3dd0c..4c2b60e9 100644 --- a/packages/partyserver/src/index.ts +++ b/packages/partyserver/src/index.ts @@ -291,8 +291,8 @@ function decodeProps(header: string): unknown { */ export async function getServerByName< Env extends Cloudflare.Env = Cloudflare.Env, - T extends Server = Server, - Props extends Record = Record + T extends Server = Server, + Props extends object = Record >( serverNamespace: DurableObjectNamespace, name: string, @@ -428,8 +428,8 @@ function resolveCorsHeaders( export async function routePartykitRequest< Env extends Cloudflare.Env = Cloudflare.Env, - T extends Server = Server, - Props extends Record = Record + T extends Server = Server, + Props extends object = Record >( req: Request, env: Env = defaultEnv as Env, @@ -586,9 +586,15 @@ Did you forget to add a durable object binding to the class ${namespace[0].toUpp } } +/** + * @template Env Environment type containing bindings + * @template Props Initial-props type delivered to `onStart()`. Bounded by + * `object` rather than `Record` so user-defined interfaces + * qualify — interfaces have no implicit index signature. + */ export class Server< Env extends Cloudflare.Env = Cloudflare.Env, - Props extends Record = Record + Props extends object = Record > extends DurableObject { static options: { hibernate?: boolean } = { hibernate: false diff --git a/packages/partyserver/src/tests/props.test-d.ts b/packages/partyserver/src/tests/props.test-d.ts new file mode 100644 index 00000000..b1d9223a --- /dev/null +++ b/packages/partyserver/src/tests/props.test-d.ts @@ -0,0 +1,53 @@ +/** + * Type tests for Server props typing. Compile-time only — this file is + * typechecked but never executed (vitest only picks up `*.test.ts`). + * + * Interfaces do not get implicit index signatures in TypeScript, so a + * `Record` bound rejects user-defined interfaces with + * "Index signature for type 'string' is missing". Props bounds must accept + * plain interfaces while still rejecting non-object props. + */ +import { Server, getServerByName, routePartykitRequest } from "../index"; + +// A well-defined interface with NO index signature. +interface AuthProps { + userId: string; + permissions: string[]; +} + +declare const authProps: AuthProps; +declare const env: Cloudflare.Env; +declare const request: Request; + +// ============================================ +// POSITIVE TESTS - interface props must be accepted +// ============================================ + +// Server must be instantiable with interface Props. +declare class AuthServer extends Server {} +declare const serverNamespace: DurableObjectNamespace; + +// getServerByName must accept interface-typed props. +getServerByName(serverNamespace, "instance", { props: authProps }); + +// onStart receives the interface type. +declare const authServer: AuthServer; +authServer.onStart(authProps); + +// routePartykitRequest must accept interface-typed props. +routePartykitRequest(request, env, { props: authProps }); + +// ============================================ +// NEGATIVE TESTS - non-object props stay rejected +// ============================================ + +// @ts-expect-error — a primitive is not a props bag +declare class BadServer extends Server {} + +getServerByName(serverNamespace, "instance", { + // @ts-expect-error — a primitive is not a props bag + props: "not-an-object" +}); + +// Silence unused-declaration noise; this file only exists to typecheck. +export type {};