BA
Better Auth•3mo ago
Daniel

The inferred type of 'authClient' cannot be named without a reference...

I was trying to create a shared auth client package in my monorepo, since I have multiple frontends authenticating with the same server.. But I keep getting an error like this (twice) even before adding any plugins... Literally.. this is my entire code:
import { createAuthClient } from "better-auth/client";

export const authClient = (baseURL: string) => {
return createAuthClient({
baseURL,
});
};
import { createAuthClient } from "better-auth/client";

export const authClient = (baseURL: string) => {
return createAuthClient({
baseURL,
});
};
I can't disable declaration in my tsconfig, since it's a shared package in my monorepo, and I need the type safety. I can't add an explicit return type like ReturnType<typeof createAuthClient> since that ruins type safety for any plugins I might add.. Is this a bug in better-auth? Or did I get something wrong with the configuration
23 Replies
Daniel
DanielOP•3mo ago
I'm getting this error twice: The inferred type of 'authClient' cannot be named without a reference to '../node_modules/better-auth/dist/shared/better-auth.ClXlabtY.js'. This is likely not portable. A type annotation is necessary.ts(2742) The inferred type of 'authClient' cannot be named without a reference to '../node_modules/better-auth/dist/shared/better-auth.ZSfSbnQT.js'. This is likely not portable. A type annotation is necessary.ts(2742) There seems to be a few issues open about this from March, but they're marked as solved, which is really confusing.. I really want to work with better-auth on this project. But this is like the third typescript-related issue I ran into and it's really giving me a headache. Maybe I'm just doing something wrong.
DN_Dev
DN_Dev•3mo ago
I had similar issues, worked around this by doing things like this:
export const BETTER_AUTH_DEFAULT_SESSION = {
modelName: BETTER_AUTH_TABLE.AUTH_SESSIONS,
fields: {
userId: BETTER_AUTH_FIELD.USER_ID,
expiresAt: BETTER_AUTH_FIELD.EXPIRES_AT,
ipAddress: BETTER_AUTH_FIELD.IP_ADDRESS,
userAgent: BETTER_AUTH_FIELD.USER_AGENT,
createdAt: BETTER_AUTH_FIELD.CREATED_AT,
updatedAt: BETTER_AUTH_FIELD.UPDATED_AT,
},
disableSessionRefresh: false,
preserveSessionInDatabase: true,
storeSessionInDatabase: true,
cookieCache: {
enabled: !isDevelopmentMode(),
maxAge: SESSION_COOKIE_CACHE_MAX_AGE_IN_SECS,
},
updateAge: SESSION_UPDATE_AGE_IN_SECS,
expiresIn: SESSION_MAX_AGE_IN_SECS,
freshAge: SESSION_FRESH_AGE_IN_SECS,
} satisfies BetterAuthOptions["session"];

---

export const defaultBetterAuthConfig = {
user: { ...BETTER_AUTH_DEFAULT_USER },
session: {
...BETTER_AUTH_DEFAULT_SESSION,
},
account: {
...BETTER_AUTH_DEFAULT_ACCOUNT,
},
verification: {
...BETTER_AUTH_DEFAULT_VERIFICATION,
},
rateLimit: {
...BETTER_AUTH_DEFAULT_RATE_LIMIT,
},
emailAndPassword: {
...BETTER_AUTH_DEFAULT_EMAIL_AND_PASSWORD,
},
emailVerification: {
...BETTER_AUTH_DEFAULT_EMAIL_VERIFICATION,
},
advanced: {
...BETTER_AUTH_DEFAULT_ADVANCED,
},
onAPIError: {
...BETTER_AUTH_ON_API_ERROR,
},
} as const;
export const BETTER_AUTH_DEFAULT_SESSION = {
modelName: BETTER_AUTH_TABLE.AUTH_SESSIONS,
fields: {
userId: BETTER_AUTH_FIELD.USER_ID,
expiresAt: BETTER_AUTH_FIELD.EXPIRES_AT,
ipAddress: BETTER_AUTH_FIELD.IP_ADDRESS,
userAgent: BETTER_AUTH_FIELD.USER_AGENT,
createdAt: BETTER_AUTH_FIELD.CREATED_AT,
updatedAt: BETTER_AUTH_FIELD.UPDATED_AT,
},
disableSessionRefresh: false,
preserveSessionInDatabase: true,
storeSessionInDatabase: true,
cookieCache: {
enabled: !isDevelopmentMode(),
maxAge: SESSION_COOKIE_CACHE_MAX_AGE_IN_SECS,
},
updateAge: SESSION_UPDATE_AGE_IN_SECS,
expiresIn: SESSION_MAX_AGE_IN_SECS,
freshAge: SESSION_FRESH_AGE_IN_SECS,
} satisfies BetterAuthOptions["session"];

---

export const defaultBetterAuthConfig = {
user: { ...BETTER_AUTH_DEFAULT_USER },
session: {
...BETTER_AUTH_DEFAULT_SESSION,
},
account: {
...BETTER_AUTH_DEFAULT_ACCOUNT,
},
verification: {
...BETTER_AUTH_DEFAULT_VERIFICATION,
},
rateLimit: {
...BETTER_AUTH_DEFAULT_RATE_LIMIT,
},
emailAndPassword: {
...BETTER_AUTH_DEFAULT_EMAIL_AND_PASSWORD,
},
emailVerification: {
...BETTER_AUTH_DEFAULT_EMAIL_VERIFICATION,
},
advanced: {
...BETTER_AUTH_DEFAULT_ADVANCED,
},
onAPIError: {
...BETTER_AUTH_ON_API_ERROR,
},
} as const;
You just have to break the config down into smaller parts, only the keys would not be type safe but I found that to be fine And for the plugins, I did
export const baseDefaultUsernamePlugin = Object.freeze({
schema: {
user: {
modelName: BETTER_AUTH_TABLE.AUTH_USERS,
fields: { displayUsername: BETTER_AUTH_FIELD.DISPLAY_USERNAME },
},
},
minUsernameLength: USERNAME_MIN_LENGTH,
maxUsernameLength: USERNAME_MAX_LENGTH,
} satisfies Parameters<typeof username>[0]);
export const baseDefaultUsernamePlugin = Object.freeze({
schema: {
user: {
modelName: BETTER_AUTH_TABLE.AUTH_USERS,
fields: { displayUsername: BETTER_AUTH_FIELD.DISPLAY_USERNAME },
},
},
minUsernameLength: USERNAME_MIN_LENGTH,
maxUsernameLength: USERNAME_MAX_LENGTH,
} satisfies Parameters<typeof username>[0]);
Spread your configs into your betterAuth function in each app that uses it, and you should have decent enough type safety there
Daniel
DanielOP•3mo ago
This just seems like terrible developer experience 🥲 I'm not sure if I fully understand yet.. but I did something like:
import { createAuthClient } from "better-auth/client";
import { adminClient } from "better-auth/client/plugins";

const defaultConfig = {
baseURL: "http://localhost:3000",
plugins: [adminClient()],
} satisfies Parameters<typeof createAuthClient>[0];

export const authClient = (
baseURL: string
): ReturnType<typeof createAuthClient<typeof defaultConfig>> => {
return createAuthClient({
...defaultConfig,
baseURL,
});
};
import { createAuthClient } from "better-auth/client";
import { adminClient } from "better-auth/client/plugins";

const defaultConfig = {
baseURL: "http://localhost:3000",
plugins: [adminClient()],
} satisfies Parameters<typeof createAuthClient>[0];

export const authClient = (
baseURL: string
): ReturnType<typeof createAuthClient<typeof defaultConfig>> => {
return createAuthClient({
...defaultConfig,
baseURL,
});
};
for example, and the error still appears, just now for defaultConfig instead of authClient
The inferred type of 'defaultConfig' cannot be named without a reference to '../node_modules/better-auth/dist/shared/better-auth.2lcgvW_O.js'. This is likely not portable. A type annotation is necessary.ts(2742)
The inferred type of 'defaultConfig' cannot be named without a reference to '../node_modules/better-auth/dist/shared/better-auth.2lcgvW_O.js'. This is likely not portable. A type annotation is necessary.ts(2742)
DN_Dev
DN_Dev•3mo ago
Yeah it's not great when it comes to this. If you spread the entire objects you'll get these issues unfortunately. Instead of satisfies Parameters<typeof createAuthClient>[0] you should use as const and type each key if you need to instead e.g.
type AuthClient = Parameters<typeof createAuthClient>[0]
const defaultConfig = {
baseURL: "http://localhost:3000" satisfies AuthClient["baseURL"],
plugins: [adminClient()] satisfies AuthClient["plugins"],
} as const
type AuthClient = Parameters<typeof createAuthClient>[0]
const defaultConfig = {
baseURL: "http://localhost:3000" satisfies AuthClient["baseURL"],
plugins: [adminClient()] satisfies AuthClient["plugins"],
} as const
Something like that...unfortunately that means you lose auto suggestion for config keys but I found that to be manageable. Each key can be copied from the indexed access type i.e. AuthClient["copy-this-here"]
Daniel
DanielOP•3mo ago
I did this, and I still don't get type safety for authClient.admin for example.. Is that what you meant? Cause then I could just set the return type explicitly to ReturnType<typeof createAuthClient>
DN_Dev
DN_Dev•3mo ago
Sorry, typing plugins explicitly like that would mean you lose type safety there. Here's an e.g. of my plugins array which is passed to my betterAuth instance.
const plugins = [
twoFactor({
...baseDefaultTwoFactorPlugin,
}),
username({
...baseDefaultUsernamePlugin,
usernameValidator: getUsernameValidator(),
}),
magicLink({
...getBaseDefaultMagicLinkPlugin({
sendMagicLink: () => {},
}),
}),
emailOTP({
...getBaseDefaultEmailOtpPlugin({
sendVerificationOTP: async () => {
return;
},
}),
}),
haveIBeenPwned({
...baseDefaultHaveIBeenPwnedPlugin,
}),
captcha({
...getBaseDefaultCaptchaPlugin({
provider: "google-recaptcha",
secretKey: ENV_SERVER.GOOGLE_RECAPTCHA_SECRET_KEY,
}),
}),
admin({ ...baseDefaultAdminPlugin }),
organization({ ...baseDefaultOrganizationPlugin }),
];
const plugins = [
twoFactor({
...baseDefaultTwoFactorPlugin,
}),
username({
...baseDefaultUsernamePlugin,
usernameValidator: getUsernameValidator(),
}),
magicLink({
...getBaseDefaultMagicLinkPlugin({
sendMagicLink: () => {},
}),
}),
emailOTP({
...getBaseDefaultEmailOtpPlugin({
sendVerificationOTP: async () => {
return;
},
}),
}),
haveIBeenPwned({
...baseDefaultHaveIBeenPwnedPlugin,
}),
captcha({
...getBaseDefaultCaptchaPlugin({
provider: "google-recaptcha",
secretKey: ENV_SERVER.GOOGLE_RECAPTCHA_SECRET_KEY,
}),
}),
admin({ ...baseDefaultAdminPlugin }),
organization({ ...baseDefaultOrganizationPlugin }),
];
Above is what you would need to do in each app that uses plugins. I export the default configs from the auth package in my monorepo. E.g.
export const baseDefaultAdminPlugin = Object.freeze({
schema: {
user: {
modelName: BETTER_AUTH_TABLE.AUTH_ADMINS,
fields: {
banned: BETTER_AUTH_FIELD.IS_BANNED,
banReason: BETTER_AUTH_FIELD.BAN_REASON,
banExpires: BETTER_AUTH_FIELD.BAN_EXPIRES_AT,
},
},
session: {
modelName: BETTER_AUTH_TABLE.AUTH_SESSIONS,
fields: { impersonatedBy: BETTER_AUTH_FIELD.IMPERSONATED_BY },
},
},
} satisfies Parameters<typeof admin>[0]);
export const baseDefaultAdminPlugin = Object.freeze({
schema: {
user: {
modelName: BETTER_AUTH_TABLE.AUTH_ADMINS,
fields: {
banned: BETTER_AUTH_FIELD.IS_BANNED,
banReason: BETTER_AUTH_FIELD.BAN_REASON,
banExpires: BETTER_AUTH_FIELD.BAN_EXPIRES_AT,
},
},
session: {
modelName: BETTER_AUTH_TABLE.AUTH_SESSIONS,
fields: { impersonatedBy: BETTER_AUTH_FIELD.IMPERSONATED_BY },
},
},
} satisfies Parameters<typeof admin>[0]);
Daniel
DanielOP•3mo ago
man. at this point I might as well just create the client in each app separately and just disable declaration in my tsconfig, since they don't export anything anyway. @bekacru any chance this will be addressed in a future update? it really sucks to have to either lose my one source of truth, or have to know how to do awesome typescript backflips like @DN_Dev
DN_Dev
DN_Dev•3mo ago
Yeah that's basically what I had to do, and export as much code as I could to each auth instance in the apps
bekacru
bekacru•3mo ago
will be addressed
sneakyf1shy
sneakyf1shy•2mo ago
was there ever any update on this?
bekacru
bekacru•2mo ago
yeah we addressed some of the issues. if you're still haivng issues, please share with me your auth client config
sneakyf1shy
sneakyf1shy•2mo ago
i only had an issue with jwt and passkey plugin i was able to fix it by explicitly typing those to just be a BetterAuthPlugin
bekacru
bekacru•2mo ago
this would break the ts inference. can you share with me your auth client config when you can
sneakyf1shy
sneakyf1shy•2mo ago
sneakyf1shy
sneakyf1shy•2mo ago
@bekacru jwt breaks due to jose, passkey due to some webauthn library this setup right now works for me because the clients infer from their plugins and not from the server but would be nice to have it fixed in the future also i did move this to a seperate package and have it compiled, cause i was getting errors like 'stripeCustomerId' doesnt exist on user due to the type inference depth/complexity. compiling/composite helped quite a bit
bekacru
bekacru•2mo ago
I see. Will look into it. For the time being I recommend casting as ReturnType<typeof jwt> instead were you generating decalation files? if dts is set to false that shouldn't happen
sneakyf1shy
sneakyf1shy•2mo ago
yeah i am just to speed up editor type inference this gaves the same issue interms of declaration generation "This is likely not portable. A type annotation is necessary.ts(2742)"
Ping
Ping•2mo ago
what's in your tsconfig? May I see it?
sneakyf1shy
sneakyf1shy•2mo ago
this is my old tsconfig from when i was having the error auth.ts was inside the web package and so this is apps/web/tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["vite/client"],

"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"noEmit": true,

"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/lib/db/*"],
"@/*": ["./src/*"]
},
"plugins": [{ "name": "vite-plugin-iso-import" }]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"references": [
{
"path": "../../packages/services"
}
]
}
{
"compilerOptions": {
"target": "ESNext",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["vite/client"],

"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"noEmit": true,

"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/lib/db/*"],
"@/*": ["./src/*"]
},
"plugins": [{ "name": "vite-plugin-iso-import" }]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"references": [
{
"path": "../../packages/services"
}
]
}
and now after i moved better-auth and some other items to a trpc package:
// apps/web/tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["vite/client"],

"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"noEmit": true,

"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/lib/db/*"],
"@/*": ["./src/*"]
},
"plugins": [{ "name": "vite-plugin-iso-import" }]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"references": [
{ "path": "../../packages/services" },
{ "path": "../../packages/trpc" }
]
}

// packages/trpc/tsconfig.json

{
"extends": "@uncovr/typescript-config/base.json",
"compilerOptions": {
"composite": true,
"incremental": true,
"outDir": "./dist",
"lib": ["ESNext", "DOM"],
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"references": [
{ "path": "../../packages/shared" },
{ "path": "../../packages/services" }
]
}


//packages/typescript-config/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "Preserve",
"moduleDetection": "force",
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"jsx": "react-jsx"
}
}
// apps/web/tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["vite/client"],

"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"noEmit": true,

"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/lib/db/*"],
"@/*": ["./src/*"]
},
"plugins": [{ "name": "vite-plugin-iso-import" }]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"references": [
{ "path": "../../packages/services" },
{ "path": "../../packages/trpc" }
]
}

// packages/trpc/tsconfig.json

{
"extends": "@uncovr/typescript-config/base.json",
"compilerOptions": {
"composite": true,
"incremental": true,
"outDir": "./dist",
"lib": ["ESNext", "DOM"],
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"references": [
{ "path": "../../packages/shared" },
{ "path": "../../packages/services" }
]
}


//packages/typescript-config/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "Preserve",
"moduleDetection": "force",
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"jsx": "react-jsx"
}
}
sneakyf1shy
sneakyf1shy•2mo ago
and now auth gets "compiled" like this to js+dts+dts.map
No description
sneakyf1shy
sneakyf1shy•2mo ago
i still had the error, likely now due to compiling; but as said above I had to type 2 plugins as generic plugins then it went away (jwt,passkey)
Ping
Ping•2mo ago
It could be that composite, declaration, & declarationMap are being carried over? Since these usually cause type issues with BA
sneakyf1shy
sneakyf1shy•2mo ago
actually wait; i think this whole thing started cause i wanted to move to a different package in the first place so yes, this might be related to composite/declaration/declarationMap in my case but even with this slight disadvantage, doing this sorta fixed some other issues for me like the TS server being slow, or not being able to infer at depth which would lead to stuff like 'stripeCustomerId does not exist on type User' https://ptb.discord.com/channels/1288403910284935179/1407166259652595732/1407166259652595732

Did you find this page helpful?