-
Couldn't load subscription status.
- Fork 64
Description
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:
- Infer the
Optionstype from the server-sideauthinstance - Extract the
user.additionalFieldsconfiguration (fooandtestin this case) - 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 authis inferred asAuth<BetterAuthOptions>(the base type)- Instead of
Auth<SpecificOptions>(with the customadditionalFields)
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 inferenceRoot 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:
- Infer the specific
OptionsfromcreateAuth - Preserve this type through
getStaticAuth - Make
typeof authproperly typed asAuth<SpecificOptions> - Enable
inferAdditionalFields<typeof auth>()to extract the correct fields
Impact
This affects all users of @convex-dev/better-auth who use:
- Custom
additionalFieldson 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-erroror// @ts-ignoreto 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
inferAdditionalFieldsplugin frombetter-auth/client/plugins - The same pattern is used in other Better Auth integrations and should work correctly once fixed