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

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