Skip to content

inferAdditionalFields<typeof auth>() doesn't recognize additional fields #140

@richardsolomou

Description

@richardsolomou

Description

The inferAdditionalFields<typeof auth>() plugin doesn't properly infer additional user/session fields defined in the server-side auth configuration. TypeScript fails to recognize custom fields like foo or other additionalFields defined in the betterAuth options.

This issue is visible in the Next.js example app where the foo field is passed during sign-up (examples/next/app/(unauth)/sign-up/SignUp.tsx:49) but TypeScript doesn't recognize it as a valid field.

Environment

  • @convex-dev/better-auth version: 0.9.6
  • better-auth version: 1.3.27

Reproduction

This issue can be reproduced in the existing Next.js example in this repository:

1. Server-side configuration defines additional fields

examples/next/convex/auth.ts (lines 86-96):

user: {
  additionalFields: {
    foo: {
      type: "string",
      required: false,
    },
    test: {
      type: "json",
      required: false,
    },
  },
  // ...
},

examples/next/convex/betterAuth/auth.ts:

import { getStaticAuth } from "@convex-dev/better-auth";
import { createAuth } from "../auth";

// Export a static instance for Better Auth schema generation
export const auth = getStaticAuth(createAuth);

2. Client uses inferAdditionalFields<typeof auth>()

examples/next/lib/auth-client.ts (line 15):

import { inferAdditionalFields } from "better-auth/client/plugins";
import type { auth } from "@/convex/betterAuth/auth";

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_SITE_URL!,
  plugins: [
    inferAdditionalFields<typeof auth>(),  // ❌ TypeScript doesn't recognize 'foo' and 'test'
    // ...
  ],
});

3. Sign-up component tries to use the custom field

examples/next/app/(unauth)/sign-up/SignUp.tsx (lines 41-50):

await authClient.signUp.email(
  {
    email,
    password,
    name: `${firstName} ${lastName}`,
    image: image ? await convertImageToBase64(image) : "",
    // custom field configured via user.additionalFields in
    // lib/auth.ts
    foo: "baz",  // ❌ TypeScript error: Property 'foo' does not exist
  },
  // ...
);

Expected Behavior

The inferAdditionalFields<typeof auth>() plugin should:

  1. Infer the Options type from the server-side auth instance
  2. Extract the user.additionalFields configuration (foo and test in this case)
  3. Provide proper TypeScript autocomplete and type safety for these fields in the client

During sign-up:

await authClient.signUp.email({
  email,
  password,
  name: "John Doe",
  foo: "baz",  // ✅ Should be recognized as valid (string | undefined)
});

When accessing user data:

const user = authClient.useSession().data?.user;
user.foo // ✅ Should be typed as string | undefined
user.test // ✅ Should be typed as unknown (json type)

Actual Behavior

TypeScript doesn't recognize the additional fields. The type inference fails because:

  • typeof auth is inferred as Auth<BetterAuthOptions> (the base type)
  • Instead of Auth<SpecificOptions> (with the custom additionalFields)

This causes a TypeScript error in the example app:

await authClient.signUp.email({
  email,
  password,
  name: "John Doe",
  foo: "baz",  // ❌ TypeScript error: Argument of type '{ foo: string; ... }' is not assignable...
               // ❌ Object literal may only specify known properties
});

Current Workaround

Manually passing the schema works but requires duplicating the field definitions:

inferAdditionalFields({
  user: {
    foo: { type: "string", required: false },
    test: { type: "json", required: false }
  }
}) // ✅ Works but defeats the purpose of type inference

Root Cause

The issue is in src/client/index.ts:

CreateAuth type definition

// Current (problematic)
export type CreateAuth<DataModel extends GenericDataModel> =
  | ((ctx: GenericCtx<DataModel>) => ReturnType<typeof betterAuth>)
  | ((ctx: GenericCtx<DataModel>, opts?: { optionsOnly?: boolean }) => ReturnType<typeof betterAuth>);

Problem: ReturnType<typeof betterAuth> resolves to Auth<BetterAuthOptions> with the default generic, losing the specific options type.

getStaticAuth function

// Current (problematic)
export const getStaticAuth = <DataModel extends GenericDataModel>(
  createAuth: CreateAuth<DataModel>
) => {
  return createAuth({} as any, { optionsOnly: true });
};

Problem: No generic parameter to capture and preserve the specific Options type from the createAuth function.

Proposed Solution

Add an Options generic parameter to both CreateAuth and getStaticAuth to preserve type information:

export type CreateAuth<
  DataModel extends GenericDataModel,
  Options extends BetterAuthOptions = any
> =
  | ((ctx: GenericCtx<DataModel>) => Auth<Options>)
  | ((ctx: GenericCtx<DataModel>, opts?: { optionsOnly?: boolean }) => Auth<Options>);

export const getStaticAuth = <
  DataModel extends GenericDataModel,
  Options extends BetterAuthOptions = any
>(
  createAuth: CreateAuth<DataModel, Options>
): Auth<Options> => {
  return createAuth({} as any, { optionsOnly: true });
};

This allows TypeScript to:

  1. Infer the specific Options from createAuth
  2. Preserve this type through getStaticAuth
  3. Make typeof auth properly typed as Auth<SpecificOptions>
  4. Enable inferAdditionalFields<typeof auth>() to extract the correct fields

Impact

This affects all users of @convex-dev/better-auth who use:

  • Custom additionalFields on user or session objects
  • Type-safe field access via inferAdditionalFields<typeof auth>()

Without this fix, developers must either:

  • Manually duplicate field definitions (defeats the purpose of type inference)
  • Use // @ts-expect-error or // @ts-ignore to suppress TypeScript errors
  • Not use custom additional fields at all

The issue is currently visible in the Next.js example app in this repo where foo is used but causes a TypeScript error.

Additional Context

  • This issue only affects TypeScript type inference, not runtime behavior
  • The fix is backward compatible (uses default generic parameter = any)
  • Related to the inferAdditionalFields plugin from better-auth/client/plugins
  • The same pattern is used in other Better Auth integrations and should work correctly once fixed

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions