mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-24 16:11:53 -05:00
fix(types): prevent any from collapsing base type and client inference (#8981)
This commit is contained in:
5
.changeset/violet-papayas-see.md
Normal file
5
.changeset/violet-papayas-see.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Prevent `any` from collapsing `auth.$Infer` and `auth.$ERROR_CODES`. Preserve client query typing when body is `any`.
|
||||
@@ -6,6 +6,7 @@ import type { BetterFetchResponse } from "@better-fetch/fetch";
|
||||
import type { Endpoint, InputContext, StandardSchemaV1 } from "better-call";
|
||||
import type {
|
||||
HasRequiredKeys,
|
||||
IsAny,
|
||||
Prettify,
|
||||
UnionToIntersection,
|
||||
} from "../types/helper";
|
||||
@@ -108,30 +109,35 @@ export type InferUserUpdateCtx<
|
||||
UnionToIntersection<InferAdditionalFromClient<ClientOpts, "user", "input">>
|
||||
>;
|
||||
|
||||
type InferCtxQuery<
|
||||
C extends InputContext<any, any>,
|
||||
FetchOptions extends ClientFetchOption,
|
||||
> =
|
||||
C["query"] extends Record<string, any>
|
||||
? {
|
||||
query: C["query"];
|
||||
fetchOptions?: FetchOptions | undefined;
|
||||
}
|
||||
: C["query"] extends Record<string, any> | undefined
|
||||
? {
|
||||
query?: C["query"] | undefined;
|
||||
fetchOptions?: FetchOptions | undefined;
|
||||
}
|
||||
: {
|
||||
fetchOptions?: FetchOptions | undefined;
|
||||
};
|
||||
|
||||
export type InferCtx<
|
||||
C extends InputContext<any, any>,
|
||||
FetchOptions extends ClientFetchOption,
|
||||
> = 0 extends 1 & C["body"]
|
||||
? {
|
||||
fetchOptions?: FetchOptions | undefined;
|
||||
}
|
||||
: C["body"] extends Record<string, any>
|
||||
? C["body"] & {
|
||||
fetchOptions?: FetchOptions | undefined;
|
||||
}
|
||||
: C["query"] extends Record<string, any>
|
||||
? {
|
||||
query: C["query"];
|
||||
> =
|
||||
IsAny<C["body"]> extends true
|
||||
? InferCtxQuery<C, FetchOptions>
|
||||
: C["body"] extends Record<string, any>
|
||||
? C["body"] & {
|
||||
fetchOptions?: FetchOptions | undefined;
|
||||
}
|
||||
: C["query"] extends Record<string, any> | undefined
|
||||
? {
|
||||
query?: C["query"] | undefined;
|
||||
fetchOptions?: FetchOptions | undefined;
|
||||
}
|
||||
: {
|
||||
fetchOptions?: FetchOptions | undefined;
|
||||
};
|
||||
: InferCtxQuery<C, FetchOptions>;
|
||||
|
||||
export type MergeRoutes<T> = UnionToIntersection<T>;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type IsAny<T> = 0 extends 1 & T ? true : false;
|
||||
|
||||
export type Prettify<T> = Omit<T, never>;
|
||||
|
||||
export type PrettifyDeep<T> = {
|
||||
@@ -29,12 +31,41 @@ export type RequiredKeysOf<BaseType extends object> = Exclude<
|
||||
undefined
|
||||
>;
|
||||
|
||||
export type HasRequiredKeys<BaseType> = 0 extends 1 & BaseType
|
||||
? false
|
||||
: [BaseType] extends [object]
|
||||
? RequiredKeysOf<BaseType & object> extends never
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
export type HasRequiredKeys<BaseType> =
|
||||
IsAny<BaseType> extends true
|
||||
? false
|
||||
: [BaseType] extends [object]
|
||||
? RequiredKeysOf<BaseType & object> extends never
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
|
||||
export type StripEmptyObjects<T extends object> = { [K in keyof T]: T[K] };
|
||||
|
||||
/**
|
||||
* Extracts a Record-typed field from a plugin, guarding against `any`.
|
||||
*/
|
||||
export type ExtractPluginField<T, Field extends string> =
|
||||
IsAny<T> extends true
|
||||
? {}
|
||||
: T extends { [K in Field]?: Record<string, any> }
|
||||
? T[Field] extends Record<string, any>
|
||||
? T[Field]
|
||||
: {}
|
||||
: {};
|
||||
|
||||
/**
|
||||
* Walks a plugin tuple with tail-recursive accumulator (TS 4.5+),
|
||||
* extracting and intersecting the given field from each element.
|
||||
*/
|
||||
export type InferPluginFieldFromTuple<
|
||||
T extends readonly unknown[],
|
||||
Field extends string,
|
||||
Acc = {},
|
||||
> = T extends readonly [infer Head, ...infer Tail]
|
||||
? InferPluginFieldFromTuple<
|
||||
Tail,
|
||||
Field,
|
||||
Acc & ExtractPluginField<Head, Field>
|
||||
>
|
||||
: Acc;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core";
|
||||
import type { BetterAuthOptions } from "@better-auth/core";
|
||||
import type {
|
||||
InferDBFieldsFromOptionsInput,
|
||||
InferDBFieldsFromPluginsInput,
|
||||
} from "@better-auth/core/db";
|
||||
import type { UnionToIntersection } from "./helper";
|
||||
import type {
|
||||
ExtractPluginField,
|
||||
InferPluginFieldFromTuple,
|
||||
UnionToIntersection,
|
||||
} from "./helper";
|
||||
|
||||
export type AdditionalUserFieldsInput<Options extends BetterAuthOptions> =
|
||||
InferDBFieldsFromPluginsInput<"user", Options["plugins"]> &
|
||||
@@ -14,15 +18,11 @@ export type AdditionalSessionFieldsInput<Options extends BetterAuthOptions> =
|
||||
InferDBFieldsFromOptionsInput<Options["session"]>;
|
||||
|
||||
export type InferPluginTypes<O extends BetterAuthOptions> =
|
||||
O["plugins"] extends Array<infer P>
|
||||
? UnionToIntersection<
|
||||
P extends BetterAuthPlugin
|
||||
? P["$Infer"] extends Record<string, any>
|
||||
? P["$Infer"]
|
||||
: {}
|
||||
: {}
|
||||
>
|
||||
: {};
|
||||
O["plugins"] extends readonly [unknown, ...unknown[]]
|
||||
? InferPluginFieldFromTuple<O["plugins"], "$Infer">
|
||||
: O["plugins"] extends Array<infer P>
|
||||
? UnionToIntersection<ExtractPluginField<P, "$Infer">>
|
||||
: {};
|
||||
|
||||
export type {
|
||||
Account,
|
||||
|
||||
@@ -5,7 +5,11 @@ import type {
|
||||
} from "@better-auth/core";
|
||||
|
||||
import type { BetterAuthPluginDBSchema } from "@better-auth/core/db";
|
||||
import type { UnionToIntersection } from "./helper";
|
||||
import type {
|
||||
ExtractPluginField,
|
||||
InferPluginFieldFromTuple,
|
||||
UnionToIntersection,
|
||||
} from "./helper";
|
||||
|
||||
export type InferOptionSchema<S extends BetterAuthPluginDBSchema> =
|
||||
S extends Record<string, { fields: infer Fields }>
|
||||
@@ -22,15 +26,11 @@ export type InferOptionSchema<S extends BetterAuthPluginDBSchema> =
|
||||
: never;
|
||||
|
||||
export type InferPluginErrorCodes<O extends BetterAuthOptions> =
|
||||
O["plugins"] extends Array<infer P>
|
||||
? UnionToIntersection<
|
||||
P extends BetterAuthPlugin
|
||||
? P["$ERROR_CODES"] extends Record<string, any>
|
||||
? P["$ERROR_CODES"]
|
||||
: {}
|
||||
: {}
|
||||
>
|
||||
: {};
|
||||
O["plugins"] extends readonly [unknown, ...unknown[]]
|
||||
? InferPluginFieldFromTuple<O["plugins"], "$ERROR_CODES">
|
||||
: O["plugins"] extends Array<infer P>
|
||||
? UnionToIntersection<ExtractPluginField<P, "$ERROR_CODES">>
|
||||
: {};
|
||||
|
||||
export type InferPluginIDs<O extends BetterAuthOptions> =
|
||||
O["plugins"] extends Array<infer P>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BetterAuthPlugin } from "@better-auth/core";
|
||||
import type { InputContext } from "better-call";
|
||||
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||
import { createAuthEndpoint } from "../api";
|
||||
import type { InferCtx } from "../client/path-to-object";
|
||||
@@ -241,10 +240,44 @@ describe("HasRequiredKeys", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("InferCtx", () => {
|
||||
it("should preserve fetchOptions when body is any", () => {
|
||||
type Result = InferCtx<InputContext<any, any> & { body: any }, {}>;
|
||||
type Keys = keyof Result;
|
||||
expectTypeOf<Keys>().toEqualTypeOf<"fetchOptions">();
|
||||
describe("any-poisoning guards", () => {
|
||||
/**
|
||||
* InferCtx: when body is `any`, query typing should be preserved
|
||||
* via InferCtxQuery delegation instead of collapsing to `any`.
|
||||
*/
|
||||
it("InferCtx should preserve query when body is any", () => {
|
||||
type Result = InferCtx<
|
||||
{ body: any; query: { page: number }; method: "GET" },
|
||||
{}
|
||||
>;
|
||||
expectTypeOf<Result["query"]>().toEqualTypeOf<{ page: number }>();
|
||||
});
|
||||
|
||||
/**
|
||||
* InferPluginTypes: an untyped plugin (`{} as any`) in the plugins array
|
||||
* should not collapse auth.$Infer to `any`.
|
||||
*/
|
||||
it("auth.$Infer should not collapse with untyped plugin", async () => {
|
||||
const untypedPlugin = {} as any;
|
||||
const { auth } = await getTestInstance({
|
||||
plugins: [organization(), untypedPlugin],
|
||||
});
|
||||
type Infer = typeof auth.$Infer;
|
||||
expectTypeOf<Infer>().not.toBeAny();
|
||||
expectTypeOf<Infer>().toHaveProperty("Session");
|
||||
});
|
||||
|
||||
/**
|
||||
* InferPluginErrorCodes: same guard as InferPluginTypes,
|
||||
* auth.$ERROR_CODES should not collapse to `any`.
|
||||
*/
|
||||
it("auth.$ERROR_CODES should not collapse with untyped plugin", async () => {
|
||||
const untypedPlugin = {} as any;
|
||||
const { auth } = await getTestInstance({
|
||||
plugins: [organization(), untypedPlugin],
|
||||
});
|
||||
type Codes = (typeof auth)["$ERROR_CODES"];
|
||||
expectTypeOf<Codes>().not.toBeAny();
|
||||
expectTypeOf<Codes>().toHaveProperty("SESSION_EXPIRED");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user