Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/olive-frogs-invent.md
Original file line number Diff line number Diff line change
@@ -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<string, unknown>` 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<Env, object>` so subclasses declaring interface `Props` satisfy them too. Generic defaults stay `Record<string, unknown>`, so untyped usage still reads props values as `unknown`.
16 changes: 11 additions & 5 deletions packages/partyserver/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@ function decodeProps(header: string): unknown {
*/
export async function getServerByName<
Env extends Cloudflare.Env = Cloudflare.Env,
T extends Server<Env> = Server<Env>,
Props extends Record<string, unknown> = Record<string, unknown>
T extends Server<Env, object> = Server<Env>,
Props extends object = Record<string, unknown>
>(
serverNamespace: DurableObjectNamespace<T>,
name: string,
Expand Down Expand Up @@ -428,8 +428,8 @@ function resolveCorsHeaders(

export async function routePartykitRequest<
Env extends Cloudflare.Env = Cloudflare.Env,
T extends Server<Env> = Server<Env>,
Props extends Record<string, unknown> = Record<string, unknown>
T extends Server<Env, object> = Server<Env>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

shouldn't this be Server<Env, Props> to ensure that the props relate to the same type than the server type ?
Or maybe inferring the default Props type from the Server type instead ?

Props extends object = Record<string, unknown>
>(
req: Request,
env: Env = defaultEnv as Env,
Expand Down Expand Up @@ -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<string, unknown>` so user-defined interfaces
* qualify — interfaces have no implicit index signature.
*/
export class Server<
Env extends Cloudflare.Env = Cloudflare.Env,
Props extends Record<string, unknown> = Record<string, unknown>
Props extends object = Record<string, unknown>
> extends DurableObject<Env> {
static options: { hibernate?: boolean } = {
hibernate: false
Expand Down
53 changes: 53 additions & 0 deletions packages/partyserver/src/tests/props.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>` 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<Cloudflare.Env, AuthProps> {}
declare const serverNamespace: DurableObjectNamespace<AuthServer>;

// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This should have a negative test where you pass an object to props, but with a structure not matching AuthProps (i.e. missing the userId for instance), to validate that it actually provides type safety (the existing negative tests are probably already passing with the old version of types)

// ============================================

// @ts-expect-error — a primitive is not a props bag
declare class BadServer extends Server<Cloudflare.Env, string> {}

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 {};
Loading