Set the correct Typescript type when using plugins

I'm using the admin and organization plugins from better-auth/plugins. I'm explicitly typing by betterAuth instance as ReturnType<typeof betterAuth>;, but when I try to access one of the API provided functions, e.g. betterAuth.api.listUsers, TS warns me it doesn't exist. What is the correct typing for my betterAuth instance?
18 Replies
Ping
Ping3mo ago
When inferring from a returnType of a function, it doesn't work with the generics you passed from your actual Better-Auth instance. You should't need to explicitly define the auth variable type, betterAuth should auto infer and give you the correct returnType.
dan—1106
dan—1106OP3mo ago
thanks @Ping -- I'm assigning the return value to a variable on my request locals, which I think needs to be explicitly typed (so that's it's not just any). Is it possible to construct a type manually?
Ping
Ping3mo ago
You're creating a new auth instance for each request? 🤔
Is it possible to construct a type manually?
Let me think.
dan—1106
dan—1106OP3mo ago
i have a global that is populated on first request (so hopefully I'm only constructing a new instance occasionally!) I think this works? let betterAuthGlobal: ReturnType< typeof betterAuth<{ plugins: [ReturnType<typeof admin>, ReturnType<typeof organization>]; }> >;
i have a global that is populated on first request (so hopefully I'm only constructing a new instance occasionally!)
I'm doing this because I'm deploying to Cloudflare workers, and I can only grab my database connection in a request/response cycle
Ping
Ping3mo ago
If you do this, you will then get access to the type for that auth instance specifically:
export const auth = betterAuth({/* ... */});

type AuthInstance = typeof auth;

let b = {} as AuthInstance;
export const auth = betterAuth({/* ... */});

type AuthInstance = typeof auth;

let b = {} as AuthInstance;
b here should correctly infer the plugins you defined in the auth instance. Are each request creating unique auth instances? Like plugins may differ, etc?
dan—1106
dan—1106OP3mo ago
no, static config. Created once on first request to my serverless environment and then stored on a global. my approach didn't solve the errors though
Ping
Ping3mo ago
And you're not able to define the betterAuth instance at the global level because you need access to the request to deal with your DB connection on a per req/res cycle?
dan—1106
dan—1106OP3mo ago
yeah, exactly. So I have a global and I lazy load the auth instance on first request. as a hacky workaround i could create a global instance of auth with my plugins which would provide typing!
Ping
Ping3mo ago
Give this a shot:
type AuthInstance = ReturnType<
typeof betterAuth<{
plugins: [];
}>
>;

let b = {} as AuthInstance;
type AuthInstance = ReturnType<
typeof betterAuth<{
plugins: [];
}>
>;

let b = {} as AuthInstance;
Wait, I'm stupid. LOL! Sorry, it's 6am for me, I haven't slept yet. So this doesn't work??
dan—1106
dan—1106OP3mo ago
not at all! thank for the help. that lets me assign my betterAuth instance to the variable, but when I try to access betterAuth.api.listUsers I get an error: Property 'listUsers' does not exist on type 'InferAPI<... this as a hacky workaround satisfies typescript:
const authForTyping = betterAuth({
plugins: [admin(), organization()],
});

let betterAuthGlobal: typeof authForTyping;
const authForTyping = betterAuth({
plugins: [admin(), organization()],
});

let betterAuthGlobal: typeof authForTyping;
but obviously isn't super elegant 🙂
Ping
Ping3mo ago
There is another solution, which is overly complicated, and that's to have your auth config in another file/place, then use tsc and generate the d.ts of that, and the copy the declarations that is generated as the types. But that isn't super elegant either. If you're worried that this is creating yet another auth instance, maybe you can place this in another file, then export the auth as a type. Then in your main file, import the type only. Since that file is never actually ran, that auth instance won't execute. This might just be your best bet. 😅
dan—1106
dan—1106OP3mo ago
Oh that’s a nice suggestion! thanks for all the help, appreciate it! (should I mark this as solved or leave it?)
Ping
Ping3mo ago
Solved :)
b3nab
b3nab3mo ago
Ehy hello 🙂 I have a kinda similar problem as your. But instead my config object I pass to betterAuth({ ...config }) is dynamic because I use a function to generate the betterAuth instance based on parameter of the function. What could be the possible solutions to set the correct typescript type? I have something like this:
function generateBetterAuthClient(pluginOptions: BetterAuthPluginOptions) {
const clientPlugins: BetterAuthClientPlugin[] = [twoFactorClient()]
if (pluginOptions.betterAuthPlugins?.passkey) {
clientPlugins.push(passkeyClient())
}
return createAuthClient({ plugins: clientPlugins })
}
function generateBetterAuthClient(pluginOptions: BetterAuthPluginOptions) {
const clientPlugins: BetterAuthClientPlugin[] = [twoFactorClient()]
if (pluginOptions.betterAuthPlugins?.passkey) {
clientPlugins.push(passkeyClient())
}
return createAuthClient({ plugins: clientPlugins })
}
But typescript show an error on generateBetterAuthClient saying: The inferred type of "generateBetterAuthClient" cannot be named without a reference to ".pnpm/[email protected]/node_modules/nanostores". This is likely not portable. A type annotation is necessary.
Ping
Ping3mo ago
Can you show me your tsconfig?
b3nab
b3nab3mo ago
yep sure:
{
"compilerOptions": {
"baseUrl": ".",
"lib": [
"DOM",
"DOM.Iterable",
"ES2022"
],
"rootDir": "./",
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"module": "NodeNext",
"moduleResolution": "nodenext",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"emitDeclarationOnly": true,
"target": "ES2022",
"composite": true,
"plugins": [
{
"name": "next"
}
],
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",
"./dev/next-env.d.ts"
],
}
{
"compilerOptions": {
"baseUrl": ".",
"lib": [
"DOM",
"DOM.Iterable",
"ES2022"
],
"rootDir": "./",
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"module": "NodeNext",
"moduleResolution": "nodenext",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"emitDeclarationOnly": true,
"target": "ES2022",
"composite": true,
"plugins": [
{
"name": "next"
}
],
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",
"./dev/next-env.d.ts"
],
}
Basically I'm trying to develop a plugin for Payload CMS and of course it should be like a dynamic wrapper around betterAuth. Also frontend side it will load plugins dynamically based on the plugin configuration. At first I was looking to fix the TS error with a type like this:
type BetterAuthClientWithPlugins = ReturnType<
typeof createAuthClient<{
plugins: [
ReturnType<typeof twoFactorClient>,
ReturnType<typeof passkeyClient>?
]
}>
>
type BetterAuthClientWithPlugins = ReturnType<
typeof createAuthClient<{
plugins: [
ReturnType<typeof twoFactorClient>,
ReturnType<typeof passkeyClient>?
]
}>
>
It has a limit, meaning that I need to know all the plugins that can be added here. But since everyone can develop a custom plugin this may be a limitation of the above type.
b3nab
b3nab3mo ago
mmhm the problem here seems to be this: https://github.com/microsoft/TypeScript/pull/58176#issuecomment-2052698294 So this means that TS is not able to resolve the type of nanostores because my project doesn't have that dependency but it's only used internally by better-auth.
GitHub
Check nearest package.json dependencies for possible package names ...
Implements this comment. I won&#39;t say this &quot;fixes&quot; #42873, but it does prevent it from occurring for people in workspaces with appropriately set-up explicit dependencies in...
b3nab
b3nab3mo ago
So update on this: I got rid of the TS error by adding nanostores: "0.11.3" to my package.json and import * as nanostores from "nanostores" into the file where my generateBetterAuthClient function is present and it worked. But then where I use betterAuthClient it was back to the original problem, so twoFactor was a missing key in betterAuthClient (so no correct type and auto inference not working). So I modify the array without explicitly telling the type and I add the types that TS was not able to find, like so:
import type * as nanostores from 'nanostores'
import type * as simplewebauthn from '@simplewebauthn/server'

function generateBetterAuthClient(pluginOptions: BetterAuthPluginOptions) {
// const clientPlugins: BetterAuthClientPlugin[] = [twoFactorClient()]
const clientPlugins = [] // <<< HERE! Not add a type to the const so now it should be all dynamic inferred by TS
clientPlugins.push(twoFactorClient())
if (pluginOptions.betterAuthPlugins?.passkey) {
clientPlugins.push(passkeyClient())
}
return createAuthClient<{
plugins: typeof clientPlugins
}>({ plugins: clientPlugins })
}
import type * as nanostores from 'nanostores'
import type * as simplewebauthn from '@simplewebauthn/server'

function generateBetterAuthClient(pluginOptions: BetterAuthPluginOptions) {
// const clientPlugins: BetterAuthClientPlugin[] = [twoFactorClient()]
const clientPlugins = [] // <<< HERE! Not add a type to the const so now it should be all dynamic inferred by TS
clientPlugins.push(twoFactorClient())
if (pluginOptions.betterAuthPlugins?.passkey) {
clientPlugins.push(passkeyClient())
}
return createAuthClient<{
plugins: typeof clientPlugins
}>({ plugins: clientPlugins })
}
Hope it will help others. 😄

Did you find this page helpful?