diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts index ea042b8de..c0bc767ed 100644 --- a/packages/openapi-fetch/src/index.d.ts +++ b/packages/openapi-fetch/src/index.d.ts @@ -38,7 +38,19 @@ export type QuerySerializer = ( export type BodySerializer = (body: OperationRequestBodyContent) => any; -export type ParseAs = "json" | "text" | "blob" | "arrayBuffer" | "stream"; +type BodyType = { + json: T; + text: Awaited>; + blob: Awaited>; + arrayBuffer: Awaited>; + stream: Response["body"]; +}; +export type ParseAs = keyof BodyType; +export type ParseAsResponse = O extends { + parseAs: ParseAs; +} + ? BodyType[O["parseAs"]] + : T; export interface DefaultParamsOption { params?: { @@ -62,9 +74,20 @@ export type RequestBodyOption = OperationRequestBodyContent extends never export type FetchOptions = RequestOptions & Omit; -export type FetchResponse = +/** This type helper makes the 2nd function param required if params/requestBody are required; otherwise, optional */ +export type MaybeOptionalInit< + P extends {}, + M extends keyof P, +> = HasRequiredKeys>> extends never + ? [(FetchOptions> | undefined)?] + : [FetchOptions>]; + +export type FetchResponse = | { - data: FilterKeys>, MediaType>; + data: ParseAsResponse< + FilterKeys>, MediaType>, + O + >; error?: never; response: Response; } @@ -86,157 +109,69 @@ export default function createClient( clientOptions?: ClientOptions, ): { /** Call a GET endpoint */ - GET

>( + GET< + P extends PathsWithMethod, + I extends MaybeOptionalInit, + >( url: P, - ...init: HasRequiredKeys< - FetchOptions> - > extends never - ? [(FetchOptions> | undefined)?] - : [FetchOptions>] - ): Promise< - FetchResponse< - "get" extends infer T - ? T extends "get" - ? T extends keyof Paths[P] - ? Paths[P][T] - : unknown - : never - : never - > - >; + ...init: I + ): Promise>; /** Call a PUT endpoint */ - PUT

>( + PUT< + P extends PathsWithMethod, + I extends MaybeOptionalInit, + >( url: P, - ...init: HasRequiredKeys< - FetchOptions> - > extends never - ? [(FetchOptions> | undefined)?] - : [FetchOptions>] - ): Promise< - FetchResponse< - "put" extends infer T - ? T extends "put" - ? T extends keyof Paths[P] - ? Paths[P][T] - : unknown - : never - : never - > - >; + ...init: I + ): Promise>; /** Call a POST endpoint */ - POST

>( + POST< + P extends PathsWithMethod, + I extends MaybeOptionalInit, + >( url: P, - ...init: HasRequiredKeys< - FetchOptions> - > extends never - ? [(FetchOptions> | undefined)?] - : [FetchOptions>] - ): Promise< - FetchResponse< - "post" extends infer T - ? T extends "post" - ? T extends keyof Paths[P] - ? Paths[P][T] - : unknown - : never - : never - > - >; + ...init: I + ): Promise>; /** Call a DELETE endpoint */ - DELETE

>( + DELETE< + P extends PathsWithMethod, + I extends MaybeOptionalInit, + >( url: P, - ...init: HasRequiredKeys< - FetchOptions> - > extends never - ? [(FetchOptions> | undefined)?] - : [FetchOptions>] - ): Promise< - FetchResponse< - "delete" extends infer T - ? T extends "delete" - ? T extends keyof Paths[P] - ? Paths[P][T] - : unknown - : never - : never - > - >; + ...init: I + ): Promise>; /** Call a OPTIONS endpoint */ - OPTIONS

>( + OPTIONS< + P extends PathsWithMethod, + I extends MaybeOptionalInit, + >( url: P, - ...init: HasRequiredKeys< - FetchOptions> - > extends never - ? [(FetchOptions> | undefined)?] - : [FetchOptions>] - ): Promise< - FetchResponse< - "options" extends infer T - ? T extends "options" - ? T extends keyof Paths[P] - ? Paths[P][T] - : unknown - : never - : never - > - >; + ...init: I + ): Promise>; /** Call a HEAD endpoint */ - HEAD

>( + HEAD< + P extends PathsWithMethod, + I extends MaybeOptionalInit, + >( url: P, - ...init: HasRequiredKeys< - FetchOptions> - > extends never - ? [(FetchOptions> | undefined)?] - : [FetchOptions>] - ): Promise< - FetchResponse< - "head" extends infer T - ? T extends "head" - ? T extends keyof Paths[P] - ? Paths[P][T] - : unknown - : never - : never - > - >; + ...init: I + ): Promise>; /** Call a PATCH endpoint */ - PATCH

>( + PATCH< + P extends PathsWithMethod, + I extends MaybeOptionalInit, + >( url: P, - ...init: HasRequiredKeys< - FetchOptions> - > extends never - ? [(FetchOptions> | undefined)?] - : [FetchOptions>] - ): Promise< - FetchResponse< - "patch" extends infer T - ? T extends "patch" - ? T extends keyof Paths[P] - ? Paths[P][T] - : unknown - : never - : never - > - >; + ...init: I + ): Promise>; /** Call a TRACE endpoint */ - TRACE

>( + TRACE< + P extends PathsWithMethod, + I extends MaybeOptionalInit, + >( url: P, - ...init: HasRequiredKeys< - FetchOptions> - > extends never - ? [(FetchOptions> | undefined)?] - : [FetchOptions>] - ): Promise< - FetchResponse< - "trace" extends infer T - ? T extends "trace" - ? T extends keyof Paths[P] - ? Paths[P][T] - : unknown - : never - : never - > - >; + ...init: I + ): Promise>; }; /** Serialize query params to string */ diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 23e069fa5..acd39dcf4 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -21,8 +21,8 @@ export default function createClient(clientOptions) { /** * Per-request fetch (keeps settings created in createClient() - * @param {string} url - * @param {import('./index.js').FetchOptions} fetchOptions + * @param {T} url + * @param {import('./index.js').FetchOptions} fetchOptions */ async function coreFetch(url, fetchOptions) { const { @@ -50,6 +50,7 @@ export default function createClient(clientOptions) { ); // fetch! + /** @type {RequestInit} */ const requestInit = { redirect: "follow", ...baseOptions, diff --git a/packages/openapi-fetch/test/index.test.ts b/packages/openapi-fetch/test/index.test.ts index 764a17286..6f1aa2833 100644 --- a/packages/openapi-fetch/test/index.test.ts +++ b/packages/openapi-fetch/test/index.test.ts @@ -569,32 +569,56 @@ describe("client", () => { it("text", async () => { const client = createClient(); mockFetchOnce({ status: 200, body: "{}" }); - const { data } = await client.GET("/anyMethod", { parseAs: "text" }); - expect(data).toBe("{}"); + const { data, error } = (await client.GET("/anyMethod", { + parseAs: "text", + })) satisfies { data?: string }; + if (error) { + throw new Error(`parseAs text: error`); + } + expect(data.toLowerCase()).toBe("{}"); }); it("arrayBuffer", async () => { const client = createClient(); mockFetchOnce({ status: 200, body: "{}" }); - const { data } = await client.GET("/anyMethod", { + const { data, error } = (await client.GET("/anyMethod", { parseAs: "arrayBuffer", - }); - expect(data instanceof ArrayBuffer).toBe(true); + })) satisfies { data?: ArrayBuffer }; + if (error) { + throw new Error(`parseAs arrayBuffer: error`); + } + expect(data.byteLength).toBe(2); }); it("blob", async () => { const client = createClient(); mockFetchOnce({ status: 200, body: "{}" }); - const { data } = await client.GET("/anyMethod", { parseAs: "blob" }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((data as any).constructor.name).toBe("Blob"); + const { data, error } = (await client.GET("/anyMethod", { + parseAs: "blob", + })) satisfies { data?: Blob }; + if (error) { + throw new Error(`parseAs blob: error`); + } + + expect(data.constructor.name).toBe("Blob"); }); it("stream", async () => { const client = createClient(); mockFetchOnce({ status: 200, body: "{}" }); - const { data } = await client.GET("/anyMethod", { parseAs: "stream" }); + const { data } = (await client.GET("/anyMethod", { + parseAs: "stream", + })) satisfies { data?: ReadableStream | null }; + if (!data) { + throw new Error(`parseAs stream: error`); + } + expect(data instanceof Buffer).toBe(true); + if (!(data instanceof Buffer)) { + throw Error("Data should be an instance of Buffer in Node context"); + } + + expect(data.byteLength).toBe(2); }); }); }); diff --git a/packages/openapi-fetch/test/v7-beta.test.ts b/packages/openapi-fetch/test/v7-beta.test.ts index 871bcd62c..542da7b86 100644 --- a/packages/openapi-fetch/test/v7-beta.test.ts +++ b/packages/openapi-fetch/test/v7-beta.test.ts @@ -578,23 +578,30 @@ describe("client", () => { it("text", async () => { const client = createClient(); mockFetchOnce({ status: 200, body: "{}" }); - const { data } = await client.GET("/anyMethod", { parseAs: "text" }); + const { data }: { data?: string } = await client.GET("/anyMethod", { + parseAs: "text", + }); expect(data).toBe("{}"); }); it("arrayBuffer", async () => { const client = createClient(); mockFetchOnce({ status: 200, body: "{}" }); - const { data } = await client.GET("/anyMethod", { - parseAs: "arrayBuffer", - }); + const { data }: { data?: ArrayBuffer } = await client.GET( + "/anyMethod", + { + parseAs: "arrayBuffer", + }, + ); expect(data instanceof ArrayBuffer).toBe(true); }); it("blob", async () => { const client = createClient(); mockFetchOnce({ status: 200, body: "{}" }); - const { data } = await client.GET("/anyMethod", { parseAs: "blob" }); + const { data }: { data?: Blob } = await client.GET("/anyMethod", { + parseAs: "blob", + }); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((data as any).constructor.name).toBe("Blob"); }); @@ -602,7 +609,10 @@ describe("client", () => { it("stream", async () => { const client = createClient(); mockFetchOnce({ status: 200, body: "{}" }); - const { data } = await client.GET("/anyMethod", { parseAs: "stream" }); + const { data }: { data?: ReadableStream | null } = await client.GET( + "/anyMethod", + { parseAs: "stream" }, + ); expect(data instanceof Buffer).toBe(true); }); }); diff --git a/packages/openapi-typescript-helpers/index.d.ts b/packages/openapi-typescript-helpers/index.d.ts index b7f851f11..d47bcb0d1 100644 --- a/packages/openapi-typescript-helpers/index.d.ts +++ b/packages/openapi-typescript-helpers/index.d.ts @@ -77,9 +77,7 @@ export type ErrorResponse = FilterKeys< // Generic TS utils /** Find first match of multiple keys */ -export type FilterKeys = { - [K in keyof Obj]: K extends Matchers ? Obj[K] : never; -}[keyof Obj]; +export type FilterKeys = Obj[keyof Obj & Matchers]; /** Return any `[string]/[string]` media type (important because openapi-fetch allows any content response, not just JSON-like) */ export type MediaType = `${string}/${string}`; /** Filter objects that have required keys */