From a4662e957ca45113bae7aec82a3ca37b62906e49 Mon Sep 17 00:00:00 2001 From: Iulian Dragomirescu Date: Mon, 6 Apr 2026 10:22:38 +0300 Subject: [PATCH] fix(client): correct return types when throw option is true When throw: true is set in client options, the response type should be the data directly, not { data, error } wrapper. This fix uses the third generic parameter of BetterFetchResponse to properly infer the return type based on the throw option. --- packages/better-call/src/client.test.ts | 25 +++++++++++++++++++++++++ packages/better-call/src/client.ts | 24 +++++++++++++++++------- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/better-call/src/client.test.ts b/packages/better-call/src/client.test.ts index 90de5fc..db18280 100644 --- a/packages/better-call/src/client.test.ts +++ b/packages/better-call/src/client.test.ts @@ -332,4 +332,29 @@ describe("client", () => { }); client("/test2", { body: undefined, query: undefined, params: undefined }); }); + + it("should return unwrapped type T when throw: true is set", async () => { + const itemEndpoint = createEndpoint( + "/item", + { method: "GET" }, + async () => ({ id: "123", name: "test" }), + ); + + const router = createRouter({ itemEndpoint }); + + const client = createClient({ + baseURL: "http://localhost:3000", + customFetchImpl: async (url, init) => { + return router.handler(new Request(url, init)); + }, + }); + + const res = await client("/item", { + throw: true, + }); + + expect(res).toMatchObject({ id: "123", name: "test" }); + + expectTypeOf(res).toExtend<{ id: string; name: string }>(); + }); }); diff --git a/packages/better-call/src/client.ts b/packages/better-call/src/client.ts index 976095f..9a3c7d2 100644 --- a/packages/better-call/src/client.ts +++ b/packages/better-call/src/client.ts @@ -97,8 +97,11 @@ export type RequiredOptionKeys< params: true; }); -export const createClient = ( - options?: ClientOptions, +export const createClient = < + R extends Router | Router["endpoints"], + GlobalOpts extends ClientOptions = ClientOptions, +>( + options?: GlobalOpts, ) => { const fetch = createFetch(options ?? {}); type API = InferClientRoutes< @@ -121,25 +124,32 @@ export const createClient = ( OPT extends O, K extends keyof OPT, C extends InferContext, + CallOpts extends BetterFetchOption, >( path: K, - ...options: HasRequired extends true + ...callOptions: HasRequired extends true ? [ WithRequired< BetterFetchOption, keyof RequiredOptionKeys >, ] - : [BetterFetchOption?] + : [CallOpts?] ): Promise< BetterFetchResponse< - Awaited> + Awaited>, + unknown, + CallOpts["throw"] extends boolean + ? CallOpts["throw"] + : GlobalOpts["throw"] extends true + ? true + : false > > => { return (await fetch(path as string, { - ...options[0], + ...callOptions[0], })) as any; }; }; -export * from "./error"; +export * from "./error"; \ No newline at end of file