[GH-ISSUE #8986] [Types] use const generic on betterAuth() to preserve plugin tuple types #28573

Open
opened 2026-04-17 20:01:10 -05:00 by GiteaMirror · 0 comments
Owner

Originally created by @cursor[bot] on GitHub (Apr 6, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8986

Problem

plugins in BetterAuthOptions is typed as BetterAuthPlugin[] (init-options.ts#L773), so when an untyped (any) plugin is present alongside typed plugins:

const auth = betterAuth({
  plugins: [organization(), untypedPlugin], // untypedPlugin: any
});

TypeScript collapses the inferred array element type: Organization | anyany, producing any[]. This means Options["plugins"] loses all individual plugin type information before the type-level tuple walk (InferPluginFieldFromTuple) gets a chance to process each element separately.

Current mitigations (PR #8981)

PR #8981 added IsAny guards to ExtractPluginField and related utilities, which prevents the result from collapsing to any. However, because the tuple information is already lost at the BetterAuthPlugin[] level, all typed plugin contributions are also lost — the result is {} rather than the org plugin's inferred types.

Proposed solution

Add a const modifier to the generic type parameter of betterAuth():

export const betterAuth = <const Options extends BetterAuthOptions>(
  options: Options & {},
): Auth<Options> => { ... };

With const (TypeScript 5.0+, TS#51865):

  1. Tuple structure is preserved: [organization(), {} as any] infers as readonly [OrgPlugin, any] instead of any[]
  2. Tuple walk path activates: O["plugins"] extends readonly [unknown, ...unknown[]] matches, routing to InferPluginFieldFromTuple
  3. Per-element any isolation works: InferPluginFieldFromTuple processes each element via ExtractPluginField, which has the IsAny guard — any elements return {}, typed elements preserve their contributions

Expected outcome

const auth = betterAuth({
  plugins: [organization(), {} as any],
});

// Before (current): auth.$Infer = {} (all contributions lost)
// After (const generic): auth.$Infer = { Session: { ... activeOrganizationId ... } }

Considerations

  • The const modifier affects the entire Options type, not just plugins. This may over-narrow other properties (e.g., string config values becoming literal types like "postgres" instead of string). Need to verify this does not break downstream type consumers.
  • Arrays inferred with const become readonly. The existing tuple check readonly [unknown, ...unknown[]] already handles this, but other places that consume Options["plugins"] may need to accept readonly arrays.
  • The same const treatment may be needed for createBetterAuth in packages/better-auth/src/auth/base.ts and the minimal variant in packages/better-auth/src/auth/minimal.ts.
  • Need to ensure getTestInstance (test utility) also preserves tuple types for tests to validate this behavior.
  • InferDBFieldsFromPlugins / InferDBFieldsFromPluginsInput in packages/core/src/db/type.ts also use tuple walks and would benefit from this change.

References

Originally created by @cursor[bot] on GitHub (Apr 6, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8986 ## Problem `plugins` in `BetterAuthOptions` is typed as `BetterAuthPlugin[]` ([init-options.ts#L773](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts#L773)), so when an untyped (`any`) plugin is present alongside typed plugins: ```ts const auth = betterAuth({ plugins: [organization(), untypedPlugin], // untypedPlugin: any }); ``` TypeScript collapses the inferred array element type: `Organization | any` → `any`, producing `any[]`. This means `Options["plugins"]` loses all individual plugin type information **before** the type-level tuple walk (`InferPluginFieldFromTuple`) gets a chance to process each element separately. ### Current mitigations (PR #8981) PR #8981 added `IsAny` guards to `ExtractPluginField` and related utilities, which prevents the result from collapsing to `any`. However, because the tuple information is already lost at the `BetterAuthPlugin[]` level, **all typed plugin contributions are also lost** — the result is `{}` rather than the org plugin's inferred types. ## Proposed solution Add a `const` modifier to the generic type parameter of `betterAuth()`: ```ts export const betterAuth = <const Options extends BetterAuthOptions>( options: Options & {}, ): Auth<Options> => { ... }; ``` With `const` (TypeScript 5.0+, [TS#51865](https://github.com/microsoft/TypeScript/pull/51865)): 1. **Tuple structure is preserved**: `[organization(), {} as any]` infers as `readonly [OrgPlugin, any]` instead of `any[]` 2. **Tuple walk path activates**: `O["plugins"] extends readonly [unknown, ...unknown[]]` matches, routing to `InferPluginFieldFromTuple` 3. **Per-element `any` isolation works**: `InferPluginFieldFromTuple` processes each element via `ExtractPluginField`, which has the `IsAny` guard — `any` elements return `{}`, typed elements preserve their contributions ### Expected outcome ```ts const auth = betterAuth({ plugins: [organization(), {} as any], }); // Before (current): auth.$Infer = {} (all contributions lost) // After (const generic): auth.$Infer = { Session: { ... activeOrganizationId ... } } ``` ## Considerations - The `const` modifier affects the **entire** `Options` type, not just `plugins`. This may over-narrow other properties (e.g., string config values becoming literal types like `"postgres"` instead of `string`). Need to verify this does not break downstream type consumers. - Arrays inferred with `const` become `readonly`. The existing tuple check `readonly [unknown, ...unknown[]]` already handles this, but other places that consume `Options["plugins"]` may need to accept `readonly` arrays. - The same `const` treatment may be needed for `createBetterAuth` in `packages/better-auth/src/auth/base.ts` and the minimal variant in `packages/better-auth/src/auth/minimal.ts`. - Need to ensure `getTestInstance` (test utility) also preserves tuple types for tests to validate this behavior. - `InferDBFieldsFromPlugins` / `InferDBFieldsFromPluginsInput` in `packages/core/src/db/type.ts` also use tuple walks and would benefit from this change. ## References - PR #8981: current `IsAny` guard approach - [TypeScript PR #51865](https://github.com/microsoft/TypeScript/pull/51865): `const` type parameters - [TypeScript PR #45711](https://github.com/microsoft/TypeScript/pull/45711): stricter conditional checks for `any` - [`BetterAuthOptions.plugins` definition](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts#L773)
GiteaMirror added the enhancement label 2026-04-17 20:01:10 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28573