Skip to content

fix: make Endpoint properties readonly and fix OpenAPI schema types#122

Merged
himself65 merged 1 commit intobetter-auth:mainfrom
gustavovalverde:fix/endpoint-type-variance
Mar 20, 2026
Merged

fix: make Endpoint properties readonly and fix OpenAPI schema types#122
himself65 merged 1 commit intobetter-auth:mainfrom
gustavovalverde:fix/endpoint-type-variance

Conversation

@gustavovalverde
Copy link
Contributor

@gustavovalverde gustavovalverde commented Mar 20, 2026

Summary

Two type-level fixes that affect consumers using strict TypeScript configurations. No runtime behavior changes.


1. Endpoint.options and Endpoint.path are now readonly

The problem:

The Endpoint type has options and path as mutable properties:

export type Endpoint<..., Meta extends EndpointMetadata | undefined = EndpointMetadata | undefined, ...> = {
    // ... call signatures ...
    options: { method: Method; metadata?: Meta };  // mutable
    path: Path;                                     // mutable
};

Because options is mutable, TypeScript treats Meta as invariant — it appears in both a readable (covariant) and writable (contravariant) position. This means a concrete Endpoint<..., { SERVER_ONLY: true }, ...> is not assignable to Endpoint<..., EndpointMetadata | undefined, ...>:

// This fails with TS2322:
const endpoints: { [key: string]: Endpoint } = {
    myEndpoint: createEndpoint("/foo", { method: "GET", metadata: { SERVER_ONLY: true } }, handler),
    //          ^^^^^^^^^^^^^^^^ Type '{ SERVER_ONLY: true }' is not assignable to 'EndpointMetadata | undefined'
};

This is a critical issue for better-auth, where BetterAuthPlugin.endpoints is typed as { [key: string]: Endpoint } and plugins produce endpoints with concrete metadata types like { SERVER_ONLY: true }, { scope: "server" }, or { openapi: { ... } }.

Making these readonly makes Meta, Method, and Path covariant — they only appear in readable positions. This allows concrete endpoint types to be assigned to wider container types.

Why this is safe at runtime: These properties are only assigned during endpoint construction inside createEndpoint() (via an as any cast at line 471), and are never mutated after. The readonly modifier accurately reflects how they're used.


2. OpenAPIParameter.schema optional properties now include | undefined

The problem:

With exactOptionalPropertyTypes: true (a strict TypeScript option), there's a distinction between:

  • format?: string — property can be absent or string, but NOT explicitly undefined
  • format?: string | undefined — property can be absent, string, OR explicitly undefined

When endpoints infer query parameter schemas from Zod types, optional fields produce string | undefined. With exactOptionalPropertyTypes, this is incompatible with format?: string:

// With exactOptionalPropertyTypes: true
const param: OpenAPIParameter = {
    in: "query",
    schema: { type: "string", format: undefined }
    //                         ^^^^^^^ Error: 'undefined' is not assignable to 'string'
};

Two type-level fixes that affect consumers using strict TypeScript:

1. Make `options` and `path` on `Endpoint` type readonly.

   These properties were mutable, making type parameters like `Meta`
   invariant. This prevented concrete endpoint types (e.g.,
   `Endpoint<..., { SERVER_ONLY: true }, ...>`) from being assigned
   to index signatures using bare `Endpoint`.

   Making them readonly makes `Meta`, `Method`, and `Path` covariant
   (read-only positions only), fixing the variance issue.

2. Add explicit `| undefined` to optional properties in
   `OpenAPIParameter.schema`.

   With `exactOptionalPropertyTypes: true`, `format?: string` means
   "absent or string" but NOT "explicitly undefined". When endpoints
   infer `format` as `string | undefined` (e.g., from optional zod
   fields), this causes a type mismatch. Adding `| undefined` makes
   the types compatible under strict optional property checking.

Runtime behavior is unchanged — these are type-level-only fixes.
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 20, 2026

Open in StackBlitz

npm i https://pkg.pr.new/better-call@122

commit: 25527bb

@himself65 himself65 merged commit c49035e into better-auth:main Mar 20, 2026
3 checks passed
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