Type inference additionalField as any #2170

Closed
opened 2026-03-13 09:31:51 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @daniel-primi on GitHub (Oct 21, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

export const auth = betterAuth({ user: { additionalFields: { status: { type: "string", defaultValue: "PENDING", input: false, }, finishedRegistration: { type: "boolean", defaultValue: false, input: false, }, }, }, } satisfies BetterAuthOptions);

`import Elysia from "elysia";
import { auth, type Session } from "./auth";
import { UnauthorizedError } from "./errors";

export const authHandle = new Elysia({
name: "auth-handle",
})
.mount(auth.handler)
.macro({
isActiveUser: {
async resolve({ request: { headers } }) {
const session = (await auth.api.getSession({
headers,
})) as Session | null;

			if (!session) throw new UnauthorizedError("USER_NOT_ALLOWED");

			if (
				session?.user.status !== "ACTIVE" ||
				!session.user.finishedRegistration
			)
				throw new UnauthorizedError("USER_NOT_ACTIVATED");

			return {
				user: session.user,
				session: session.session,
			};
		},
	},
});

`
The problem occurs because betterAuth() returns a runtime instance, but the $Infer type helpers (like auth.$Infer.Session) are not exposed as proper TypeScript types that can be referenced statically.

In TypeScript, typeof auth.$Infer.Session doesn’t actually resolve to the expected InferSession type — instead, it collapses to any because $Infer exists only at runtime (as a helper or placeholder object).

This means that the type inference pipeline breaks — the compiler has no link between the runtime auth object and its generic type definitions.

So, even though the object at runtime has session.user and session.session correctly structured, TypeScript can’t infer their compile-time types.

Current vs. Expected behavior

Image

What version of Better Auth are you using?

1.3.27

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Pro",
    "release": "10.0.22631",
    "cpuCount": 8,
    "cpuModel": "11th Gen Intel(R) Core(TM) i7-11390H @ 3.40GHz",
    "totalMemory": "15.74 GB",
    "freeMemory": "1.32 GB"
  },
  "node": {
    "version": "v20.16.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.8.1"
  },
  "frameworks": null,
  "databases": [
    {
      "name": "@prisma/client",
      "version": "^6.17.1"
    }
  ],
  "betterAuth": {
    "version": "^1.3.27",
    "config": {
      "emailAndPassword": {
        "enabled": true,
        "requireEmailVerification": true,
        "autoSignIn": false
      },
      "emailVerification": {
        "sendOnSignUp": true,
        "autoSignInAfterVerification": true
      },
      "user": {
        "additionalFields": {
          "status": {
            "type": "string",
            "defaultValue": "PENDING",
            "input": false
          },
          "finishedRegistration": {
            "type": "boolean",
            "defaultValue": false,
            "input": false
          }
        }
      },
      "basePath": "/auth",
      "plugins": [
        {
          "name": "open-api",
          "config": {
            "id": "open-api",
            "endpoints": {}
          }
        },
        {
          "name": "last-login-method",
          "config": {
            "id": "last-login-method",
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "user": {
                "fields": {
                  "lastLoginMethod": {
                    "type": "string",
                    "input": false,
                    "required": false,
                    "fieldName": "lastLoginMethod"
                  }
                }
              }
            }
          }
        },
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": "Failed to create user",
              "USER_ALREADY_EXISTS": "User already exists.",
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.",
              "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself",
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users",
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users",
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users",
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
              "BANNED_USER": "You have been banned from this application",
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user",
              "NO_DATA_TO_UPDATE": "No data to update",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users",
              "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself"
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "options": {
              "ac": {
                "statements": {
                  "organization": [
                    "update",
                    "delete"
                  ],
                  "member": [
                    "create",
                    "update",
                    "delete"
                  ],
                  "invitation": [
                    "create",
                    "cancel"
                  ],
                  "team": [
                    "create",
                    "update",
                    "delete"
                  ],
                  "ac": [
                    "create",
                    "read",
                    "update",
                    "delete"
                  ],
                  "register": [
                    "create",
                    "update",
                    "delete",
                    "approve"
                  ]
                }
              },
              "adminRoles": [
                "inmetro"
              ],
              "roles": {
                "inmetro": {
                  "statements": {
                    "register": [
                      "create",
                      "approve",
                      "delete",
                      "update"
                    ],
                    "organization": [
                      "update"
                    ],
                    "invitation": [
                      "create",
                      "cancel"
                    ],
                    "member": [
                      "create",
                      "update",
                      "delete"
                    ],
                    "team": [
                      "create",
                      "update",
                      "delete"
                    ],
                    "ac": [
                      "create",
                      "read",
                      "update",
                      "delete"
                    ]
                  }
                },
                "ipem": {
                  "statements": {
                    "register": [
                      "create"
                    ]
                  }
                },
                "permissionary": {
                  "statements": {
                    "register": [
                      "create"
                    ]
                  }
                },
                "factory": {
                  "statements": {
                    "register": [
                      "create"
                    ]
                  }
                }
              }
            }
          }
        }
      ]
    }
  }
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @daniel-primi on GitHub (Oct 21, 2025). ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce `export const auth = betterAuth({ user: { additionalFields: { status: { type: "string", defaultValue: "PENDING", input: false, }, finishedRegistration: { type: "boolean", defaultValue: false, input: false, }, }, }, } satisfies BetterAuthOptions);` `import Elysia from "elysia"; import { auth, type Session } from "./auth"; import { UnauthorizedError } from "./errors"; export const authHandle = new Elysia({ name: "auth-handle", }) .mount(auth.handler) .macro({ isActiveUser: { async resolve({ request: { headers } }) { const session = (await auth.api.getSession({ headers, })) as Session | null; if (!session) throw new UnauthorizedError("USER_NOT_ALLOWED"); if ( session?.user.status !== "ACTIVE" || !session.user.finishedRegistration ) throw new UnauthorizedError("USER_NOT_ACTIVATED"); return { user: session.user, session: session.session, }; }, }, }); ` The problem occurs because betterAuth() returns a runtime instance, but the $Infer type helpers (like auth.$Infer.Session) are not exposed as proper TypeScript types that can be referenced statically. In TypeScript, typeof auth.$Infer.Session doesn’t actually resolve to the expected InferSession<typeof auth> type — instead, it collapses to any because $Infer exists only at runtime (as a helper or placeholder object). This means that the type inference pipeline breaks — the compiler has no link between the runtime auth object and its generic type definitions. So, even though the object at runtime has session.user and session.session correctly structured, TypeScript can’t infer their compile-time types. ### Current vs. Expected behavior <img width="702" height="173" alt="Image" src="https://github.com/user-attachments/assets/c42df0d8-e0ab-45f7-b28d-c80db124c9e2" /> ### What version of Better Auth are you using? 1.3.27 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Pro", "release": "10.0.22631", "cpuCount": 8, "cpuModel": "11th Gen Intel(R) Core(TM) i7-11390H @ 3.40GHz", "totalMemory": "15.74 GB", "freeMemory": "1.32 GB" }, "node": { "version": "v20.16.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.8.1" }, "frameworks": null, "databases": [ { "name": "@prisma/client", "version": "^6.17.1" } ], "betterAuth": { "version": "^1.3.27", "config": { "emailAndPassword": { "enabled": true, "requireEmailVerification": true, "autoSignIn": false }, "emailVerification": { "sendOnSignUp": true, "autoSignInAfterVerification": true }, "user": { "additionalFields": { "status": { "type": "string", "defaultValue": "PENDING", "input": false }, "finishedRegistration": { "type": "boolean", "defaultValue": false, "input": false } } }, "basePath": "/auth", "plugins": [ { "name": "open-api", "config": { "id": "open-api", "endpoints": {} } }, { "name": "last-login-method", "config": { "id": "last-login-method", "hooks": { "after": [ {} ] }, "schema": { "user": { "fields": { "lastLoginMethod": { "type": "string", "input": false, "required": false, "fieldName": "lastLoginMethod" } } } } } }, { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": "Failed to create user", "USER_ALREADY_EXISTS": "User already exists.", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.", "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "You have been banned from this application", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user", "NO_DATA_TO_UPDATE": "No data to update", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users", "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself" }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } }, "options": { "ac": { "statements": { "organization": [ "update", "delete" ], "member": [ "create", "update", "delete" ], "invitation": [ "create", "cancel" ], "team": [ "create", "update", "delete" ], "ac": [ "create", "read", "update", "delete" ], "register": [ "create", "update", "delete", "approve" ] } }, "adminRoles": [ "inmetro" ], "roles": { "inmetro": { "statements": { "register": [ "create", "approve", "delete", "update" ], "organization": [ "update" ], "invitation": [ "create", "cancel" ], "member": [ "create", "update", "delete" ], "team": [ "create", "update", "delete" ], "ac": [ "create", "read", "update", "delete" ] } }, "ipem": { "statements": { "register": [ "create" ] } }, "permissionary": { "statements": { "register": [ "create" ] } }, "factory": { "statements": { "register": [ "create" ] } } } } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the elysia label 2026-03-13 09:31:51 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 21, 2025):

Hi @daniel-primi! I'm Dosu and I’m helping the better-auth team.

This is a known limitation in better-auth v1.3.27: TypeScript infers additionalFields as any when using $Infer helpers like auth.$Infer.Session, because these helpers exist only at runtime and aren't exposed as proper static types. This regression started in v1.3.24 and persists in v1.3.27, so session.user and session.session lose type safety at compile time, even though they're correct at runtime reference.

Workarounds include explicitly redeclaring your auth config type and using ReturnType with betterAuth to define a proper Auth type, for example:

const config = {
  user: {
    additionalFields: {
      status: { type: "string", defaultValue: "PENDING", input: false },
      finishedRegistration: { type: "boolean", defaultValue: false, input: false },
    },
  },
} as const;

type Auth = ReturnType<typeof betterAuth<typeof config>>;
export const auth: Auth = betterAuth(config);

Alternatively, you can use Awaited<ReturnType> to get the session type, but this may break with multiple plugins or large configs reference.

Recent PRs have improved plugin-aware type inference, but full static type inference for additionalFields is not yet supported in your version. If you need strict type safety, manual type assertions or external schema tools (like drizzle-zod) are sometimes used as a fallback reference.

This is suited for GitHub, as it's a documented limitation and area for future improvement.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Oct 21, 2025): <!-- Greeting --> Hi @daniel-primi! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known limitation in better-auth v1.3.27: TypeScript infers additionalFields as any when using $Infer helpers like auth.$Infer.Session, because these helpers exist only at runtime and aren't exposed as proper static types. This regression started in v1.3.24 and persists in v1.3.27, so session.user and session.session lose type safety at compile time, even though they're correct at runtime [reference](https://github.com/better-auth/better-auth/issues/5291). Workarounds include explicitly redeclaring your auth config type and using ReturnType with betterAuth to define a proper Auth type, for example: ```ts const config = { user: { additionalFields: { status: { type: "string", defaultValue: "PENDING", input: false }, finishedRegistration: { type: "boolean", defaultValue: false, input: false }, }, }, } as const; type Auth = ReturnType<typeof betterAuth<typeof config>>; export const auth: Auth = betterAuth(config); ``` Alternatively, you can use Awaited<ReturnType<typeof auth.api.getSession>> to get the session type, but this may break with multiple plugins or large configs [reference](https://github.com/better-auth/better-auth/issues/2818). Recent PRs have improved plugin-aware type inference, but full static type inference for additionalFields is not yet supported in your version. If you need strict type safety, manual type assertions or external schema tools (like drizzle-zod) are sometimes used as a fallback [reference](https://github.com/better-auth/better-auth/issues/2089). This is suited for GitHub, as it's a documented limitation and area for future improvement. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/e85dd4c3-241e-4bfd-8b66-dd8a80ef9aea?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/e85dd4c3-241e-4bfd-8b66-dd8a80ef9aea?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/e85dd4c3-241e-4bfd-8b66-dd8a80ef9aea?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/e85dd4c3-241e-4bfd-8b66-dd8a80ef9aea?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/e85dd4c3-241e-4bfd-8b66-dd8a80ef9aea?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/e85dd4c3-241e-4bfd-8b66-dd8a80ef9aea?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/e85dd4c3-241e-4bfd-8b66-dd8a80ef9aea?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5462)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2170