From 8b6138773324da2e8acc8071ae86d5744bd938e1 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Tue, 18 Feb 2025 15:25:55 +0100 Subject: [PATCH] fix: add client response type inference when options.throw is true --- src/client.test.ts | 54 +++++++++++++++++++++++++++++ src/client.ts | 85 +++++++++++++++++++++++++++++++--------------- 2 files changed, 111 insertions(+), 28 deletions(-) diff --git a/src/client.test.ts b/src/client.test.ts index bf8faec..589b6c7 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -214,4 +214,58 @@ describe("client", () => { }); expectTypeOf[0]>().toMatchTypeOf<"@post/test">(); }); + + it("should infer response depending on options.throw", async () => { + const endpoint = createEndpoint( + "/test", + { + method: "POST", + body: z.object({ + hello: z.string(), + }), + }, + async () => { + return { + status: 200, + body: { + hello: "world", + }, + }; + }, + ); + + const router = createRouter({ + endpoint, + }); + + const client = createClient({ + baseURL: "http://localhost:3000", + customFetchImpl: async () => { + return new Response(null); + }, + }); + + const throwingClient = createClient({ + baseURL: "http://localhost:3000", + customFetchImpl: async () => { + return new Response(null); + }, + throw: true, + }); + + const response = await client("@post/test", { + body: { + hello: "world", + }, + }); + + const throwingResponse = await throwingClient("@post/test", { + body: { + hello: "world", + }, + }); + + expectTypeOf(response.data).toMatchTypeOf<{ body: { hello: string } } | null>(); + expectTypeOf(throwingResponse).toMatchTypeOf<{ body: { hello: string } }>(); + }); }); diff --git a/src/client.ts b/src/client.ts index d88cc63..10b0aa6 100644 --- a/src/client.ts +++ b/src/client.ts @@ -55,37 +55,66 @@ export type RequiredOptionKeys< params: true; }); -export const createClient = (options: ClientOptions) => { +export function createClient( + options: ClientOptions & { throw: true }, +): FetchClientReturn; + +export function createClient( + options: ClientOptions & { throw: false }, +): FetchClientReturn; + +export function createClient( + options: ClientOptions, +): FetchClientReturn; + +export function createClient( + options: ClientOptions, +): FetchClientReturn { const fetch = createFetch(options); - type API = R extends { endpoints: Record } ? R["endpoints"] : R; - type Options = API extends { - [key: string]: infer T; - } - ? T extends Endpoint - ? { - [key in T["options"]["method"] extends "GET" - ? T["path"] - : `@${T["options"]["method"] extends string ? Lowercase : never}${T["path"]}`]: T; - } - : {} - : {}; - type O = Prettify>; - return async >( - path: K, - ...options: HasRequired extends true - ? [ - WithRequired< - BetterFetchOption, - keyof RequiredOptionKeys - >, - ] - : [BetterFetchOption?] - ): Promise< - BetterFetchResponse>> - > => { + return async (path, ...options) => { return (await fetch(path as string, { ...options[0], })) as any; }; -}; +} + +type FetchClientAPI = R extends { + endpoints: Record; +} + ? R["endpoints"] + : R; + +type FetchClientOptions = FetchClientAPI extends { + [key: string]: infer T; +} + ? T extends Endpoint + ? { + [key in T["options"]["method"] extends "GET" + ? T["path"] + : `@${T["options"]["method"] extends string ? Lowercase : never}${T["path"]}`]: T; + } + : {} + : {}; + +type FetchClientReturn = < + OPT extends Prettify>>, + K extends keyof OPT, + C extends InferContext, +>( + path: K, + ...options: HasRequired extends true + ? [ + WithRequired< + BetterFetchOption, + keyof RequiredOptionKeys + >, + ] + : [BetterFetchOption?] +) => Promise< + BetterFetchResponse< + Awaited>, + unknown, + Throw + > +>;