fix(client): name auth client return types (#10071)

This commit is contained in:
Gustavo Valverde
2026-06-14 23:45:35 -07:00
committed by GitHub
parent a787e0b66b
commit 635f190870
8 changed files with 435 additions and 183 deletions

View File

@@ -0,0 +1,5 @@
---
"better-auth": patch
---
Auth clients exported from wrapper packages can now be emitted in TypeScript declaration builds without extra type annotations.

View File

@@ -0,0 +1,179 @@
import { execFile } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
const execFileAsync = promisify(execFile);
describe("client declaration emit", () => {
const createCompositeProject = (): { dir: string; cleanup: () => void } => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ba-client-dts-"));
const betterAuthDir = path.resolve(__dirname, "../..");
const authPackageDir = path.join(dir, "packages/auth");
const webAppDir = path.join(dir, "apps/web");
fs.mkdirSync(path.join(authPackageDir, "src"), { recursive: true });
fs.mkdirSync(path.join(webAppDir, "src"), { recursive: true });
fs.mkdirSync(path.join(authPackageDir, "node_modules"), {
recursive: true,
});
fs.mkdirSync(path.join(dir, "node_modules/@repo"), { recursive: true });
fs.symlinkSync(
betterAuthDir,
path.join(authPackageDir, "node_modules/better-auth"),
"junction",
);
fs.symlinkSync(
authPackageDir,
path.join(dir, "node_modules/@repo/auth"),
"junction",
);
const compilerOptions = {
target: "ES2022",
module: "ESNext",
moduleResolution: "bundler",
lib: ["DOM", "DOM.Iterable", "ESNext"],
strict: true,
skipLibCheck: true,
composite: true,
declaration: true,
emitDeclarationOnly: true,
noEmit: false,
customConditions: ["dev-source"],
};
fs.writeFileSync(
path.join(dir, "tsconfig.json"),
JSON.stringify(
{
files: [],
references: [{ path: "./packages/auth" }, { path: "./apps/web" }],
},
null,
2,
),
);
fs.writeFileSync(
path.join(authPackageDir, "package.json"),
JSON.stringify(
{
name: "@repo/auth",
version: "0.0.0",
type: "module",
exports: {
"./client": {
types: "./dist/client.d.ts",
default: "./dist/client.js",
},
},
},
null,
2,
),
);
fs.writeFileSync(
path.join(authPackageDir, "tsconfig.json"),
JSON.stringify(
{
compilerOptions: {
...compilerOptions,
rootDir: "src",
outDir: "dist",
tsBuildInfoFile: ".tsbuildinfo",
},
include: ["src"],
},
null,
2,
),
);
fs.writeFileSync(
path.join(webAppDir, "tsconfig.json"),
JSON.stringify(
{
compilerOptions: {
...compilerOptions,
rootDir: "src",
outDir: "dist",
tsBuildInfoFile: ".tsbuildinfo",
},
references: [{ path: "../../packages/auth" }],
include: ["src"],
},
null,
2,
),
);
fs.writeFileSync(
path.join(authPackageDir, "src/client.ts"),
`
import { createAuthClient as createBetterAuthClient } from "better-auth/react";
export type AuthClientOptions = {
apiBaseUrl: string;
apiBasePath: string;
};
export const createAuthClient = ({
apiBaseUrl,
apiBasePath,
}: AuthClientOptions) =>
createBetterAuthClient({
baseURL: \`\${apiBaseUrl}\${apiBasePath}/auth\`,
});
`,
);
fs.writeFileSync(
path.join(webAppDir, "src/auth-client.ts"),
`
import { createAuthClient } from "@repo/auth/client";
export const authClient = createAuthClient({
apiBaseUrl: "http://localhost:3000",
apiBasePath: "/api",
});
authClient.useSession();
`,
);
return {
dir,
cleanup: () => fs.rmSync(dir, { recursive: true, force: true }),
};
};
/**
* @see https://github.com/better-auth/better-auth/issues/8623
*/
it("should emit declarations for auth clients re-exported through a workspace package", async () => {
const { dir, cleanup } = createCompositeProject();
try {
const tscPath = path.resolve(
__dirname,
"../../../../node_modules/.bin/tsc",
);
const { stderr } = await execFileAsync(
tscPath,
["--build", "--force", "tsconfig.json"],
{
cwd: dir,
maxBuffer: 1024 * 1024 * 10,
},
);
expect(stderr).toBe("");
} catch (error: unknown) {
const err = error as { stdout?: string; stderr?: string };
const output = `${err.stdout ?? ""}${err.stderr ?? ""}`;
expect(output).not.toContain("TS2742");
throw new Error(output);
} finally {
cleanup();
}
}, 30_000);
});

View File

@@ -43,9 +43,44 @@ type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
>
: {};
type ClientConfig = ReturnType<typeof getClientConfig>;
type ClientSession<Option extends BetterAuthClientOptions> =
InferClientAPI<Option> extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res
: never;
/**
* Lynx client returned by `createAuthClient`.
*/
export type LynxAuthClient<Option extends BetterAuthClientOptions> =
UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> &
InferActions<Option> & {
useSession: () => {
data: ClientSession<Option>;
isPending: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
};
$Infer: {
Session: NonNullable<ClientSession<Option>>;
};
$fetch: ClientConfig["$fetch"];
$store: ClientConfig["$store"];
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
export function createAuthClient<Option extends BetterAuthClientOptions>(
options?: Option | undefined,
) {
): LynxAuthClient<Option> {
const {
pluginPathMethods,
pluginsActions,
@@ -73,34 +108,7 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
atomListeners,
);
type ClientAPI = InferClientAPI<Option>;
type Session = ClientAPI extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res
: never;
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
ClientAPI &
InferActions<Option> & {
useSession: () => {
data: Session;
isPending: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
};
$Infer: {
Session: NonNullable<Session>;
};
$fetch: typeof $fetch;
$store: typeof $store;
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
return proxy as LynxAuthClient<Option>;
}
export { useStore };

View File

@@ -43,9 +43,43 @@ type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
>
: {};
type ClientConfig = ReturnType<typeof getClientConfig>;
type ClientSession<Option extends BetterAuthClientOptions> =
InferClientAPI<Option> extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res
: never;
/**
* React client returned by `createAuthClient`.
*/
export type ReactAuthClient<Option extends BetterAuthClientOptions> =
UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> &
InferActions<Option> & {
useSession: () => {
data: ClientSession<Option>;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
};
$Infer: {
Session: NonNullable<ClientSession<Option>>;
};
$fetch: ClientConfig["$fetch"];
$store: ClientConfig["$store"];
$ERROR_CODES: InferErrorCodes<Option> & typeof BASE_ERROR_CODES;
};
export function createAuthClient<Option extends BetterAuthClientOptions>(
options?: Option | undefined,
) {
): ReactAuthClient<Option> {
const {
pluginPathMethods,
pluginsActions,
@@ -73,33 +107,7 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
atomListeners,
);
type ClientAPI = InferClientAPI<Option>;
type Session = ClientAPI extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res
: never;
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
ClientAPI &
InferActions<Option> & {
useSession: () => {
data: Session;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
};
$Infer: {
Session: NonNullable<Session>;
};
$fetch: typeof $fetch;
$store: typeof $store;
$ERROR_CODES: InferErrorCodes<Option> & typeof BASE_ERROR_CODES;
};
return proxy as ReactAuthClient<Option>;
}
export { useStore };

View File

@@ -44,9 +44,46 @@ type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
>
: {};
type ClientConfig = ReturnType<typeof getClientConfig>;
type ClientSession<Option extends BetterAuthClientOptions> =
InferClientAPI<Option> extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res extends Record<string, any>
? Res
: never
: never;
/**
* Solid client returned by `createAuthClient`.
*/
export type SolidAuthClient<Option extends BetterAuthClientOptions> =
UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> &
InferActions<Option> & {
useSession: () => Accessor<{
data: ClientSession<Option>;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>;
$Infer: {
Session: NonNullable<ClientSession<Option>>;
};
$fetch: ClientConfig["$fetch"];
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
export function createAuthClient<Option extends BetterAuthClientOptions>(
options?: Option | undefined,
) {
): SolidAuthClient<Option> {
const {
pluginPathMethods,
pluginsActions,
@@ -69,36 +106,7 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
pluginsAtoms,
atomListeners,
);
type ClientAPI = InferClientAPI<Option>;
type Session = ClientAPI extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res extends Record<string, any>
? Res
: never
: never;
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> &
InferActions<Option> & {
useSession: () => Accessor<{
data: Session;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>;
$Infer: {
Session: NonNullable<Session>;
};
$fetch: typeof $fetch;
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
return proxy as SolidAuthClient<Option>;
}
export type * from "@better-fetch/fetch";

View File

@@ -39,9 +39,47 @@ type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
>
: {};
type ClientConfig = ReturnType<typeof getClientConfig>;
type ClientSession<Option extends BetterAuthClientOptions> =
InferClientAPI<Option> extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res extends Record<string, any>
? Res
: never
: never;
/**
* Svelte client returned by `createAuthClient`.
*/
export type SvelteAuthClient<Option extends BetterAuthClientOptions> =
UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> &
InferActions<Option> & {
useSession: () => Atom<{
data: ClientSession<Option>;
error: BetterFetchError | null;
isPending: boolean;
isRefetching: boolean;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>;
$fetch: ClientConfig["$fetch"];
$store: ClientConfig["$store"];
$Infer: {
Session: NonNullable<ClientSession<Option>>;
};
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
export function createAuthClient<Option extends BetterAuthClientOptions>(
options?: Option | undefined,
) {
): SvelteAuthClient<Option> {
const {
pluginPathMethods,
pluginsActions,
@@ -67,37 +105,7 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
pluginsAtoms,
atomListeners,
);
type ClientAPI = InferClientAPI<Option>;
type Session = ClientAPI extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res extends Record<string, any>
? Res
: never
: never;
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> &
InferActions<Option> & {
useSession: () => Atom<{
data: Session;
error: BetterFetchError | null;
isPending: boolean;
isRefetching: boolean;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>;
$fetch: typeof $fetch;
$store: typeof $store;
$Infer: {
Session: NonNullable<Session>;
};
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
return proxy as SvelteAuthClient<Option>;
}
export type * from "@better-fetch/fetch";

View File

@@ -39,9 +39,47 @@ type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
>
: {};
type ClientConfig = ReturnType<typeof getClientConfig>;
type ClientSession<Option extends BetterAuthClientOptions> =
InferClientAPI<Option> extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res extends Record<string, any>
? Res
: never
: never;
/**
* Client returned by `createAuthClient`.
*/
export type AuthClient<Option extends BetterAuthClientOptions> =
UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> &
InferActions<Option> & {
useSession: Atom<{
data: ClientSession<Option>;
error: BetterFetchError | null;
isPending: boolean;
isRefetching: boolean;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>;
$fetch: ClientConfig["$fetch"];
$store: ClientConfig["$store"];
$Infer: {
Session: NonNullable<ClientSession<Option>>;
};
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
export function createAuthClient<Option extends BetterAuthClientOptions>(
options?: Option | undefined,
) {
): AuthClient<Option> {
const {
pluginPathMethods,
pluginsActions,
@@ -67,39 +105,5 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
pluginsAtoms,
atomListeners,
);
type ClientAPI = InferClientAPI<Option>;
type Session = ClientAPI extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res extends Record<string, any>
? Res
: never
: never;
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
ClientAPI &
InferActions<Option> & {
useSession: Atom<{
data: Session;
error: BetterFetchError | null;
isPending: boolean;
isRefetching: boolean;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>;
$fetch: typeof $fetch;
$store: typeof $store;
$Infer: {
Session: NonNullable<Session>;
};
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
return proxy as AuthClient<Option>;
}
export type AuthClient<Option extends BetterAuthClientOptions> = ReturnType<
typeof createAuthClient<Option>
>;

View File

@@ -46,9 +46,64 @@ type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
>
: {};
type ClientConfig = ReturnType<typeof getClientConfig>;
type ClientSession<Option extends BetterAuthClientOptions> =
InferClientAPI<Option> extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res extends Record<string, any>
? Res
: never
: never;
type VueUseSession<Option extends BetterAuthClientOptions> = {
(): DeepReadonly<
Ref<{
data: ClientSession<Option>;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>
>;
<F extends (...args: any) => any>(
useFetch: F,
): Promise<{
data: Ref<ClientSession<Option>>;
isPending: false;
error: Ref<{
message?: string | undefined;
status: number;
statusText: string;
}>;
}>;
};
/**
* Vue client returned by `createAuthClient`.
*/
export type VueAuthClient<Option extends BetterAuthClientOptions> =
UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> &
InferActions<Option> & {
useSession: VueUseSession<Option>;
$Infer: {
Session: NonNullable<ClientSession<Option>>;
};
$fetch: ClientConfig["$fetch"];
$store: ClientConfig["$store"];
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
export function createAuthClient<Option extends BetterAuthClientOptions>(
options?: Option | undefined,
) {
): VueAuthClient<Option> {
const {
baseURL,
pluginPathMethods,
@@ -63,20 +118,9 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
resolvedHooks[getAtomKey(key)] = () => useStore(value);
}
type ClientAPI = InferClientAPI<Option>;
type Session = ClientAPI extends {
getSession: () => Promise<infer Res>;
}
? Res extends BetterFetchResponse<infer S>
? S
: Res extends Record<string, any>
? Res
: never
: never;
function useSession(): DeepReadonly<
Ref<{
data: Session;
data: ClientSession<Option>;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
@@ -88,7 +132,7 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
function useSession<F extends (...args: any) => any>(
useFetch: F,
): Promise<{
data: Ref<Session>;
data: Ref<ClientSession<Option>>;
isPending: false; //this is just to be consistent with the default hook
error: Ref<{
message?: string | undefined;
@@ -130,19 +174,7 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
atomListeners,
);
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> &
InferActions<Option> & {
useSession: typeof useSession;
$Infer: {
Session: NonNullable<Session>;
};
$fetch: typeof $fetch;
$store: typeof $store;
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
return proxy as VueAuthClient<Option>;
}
export type * from "@better-fetch/fetch";