diff --git a/.changeset/good-baboons-dress.md b/.changeset/good-baboons-dress.md new file mode 100644 index 0000000..c2e88ac --- /dev/null +++ b/.changeset/good-baboons-dress.md @@ -0,0 +1,5 @@ +--- +"typed-openapi": patch +--- + +fix: tanstack inference in some edge cases diff --git a/packages/typed-openapi/src/tanstack-query.generator.ts b/packages/typed-openapi/src/tanstack-query.generator.ts index 3dbec83..f7f52ba 100644 --- a/packages/typed-openapi/src/tanstack-query.generator.ts +++ b/packages/typed-openapi/src/tanstack-query.generator.ts @@ -12,7 +12,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati const file = ` import { queryOptions } from "@tanstack/react-query" - import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus } from "${ctx.relativeApiClientPath}" + import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus, TypedSuccessResponse } from "${ctx.relativeApiClientPath}" import { errorStatusCodes, TypedResponseError } from "${ctx.relativeApiClientPath}" type EndpointQueryKey = [ @@ -66,6 +66,11 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T]; + type InferResponseData = TypedSuccessResponse extends + InferResponseByStatus + ? Extract, { data: {}}>["data"] + : Extract["data"], {}>; + // // @@ -98,7 +103,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati withResponse: false as const }; const res = await this.client.${method}(path, requestParams as never); - return res as Extract, { data: {} }>["data"]; + return res as InferResponseData; }, queryKey: queryKey }), @@ -123,7 +128,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati TWithResponse extends boolean = false, TSelection = TWithResponse extends true ? InferResponseByStatus - : Extract, { data: {} }>["data"], + : InferResponseData, TError = TEndpoint extends { responses: infer TResponses } ? TResponses extends Record ? InferResponseByStatus @@ -133,7 +138,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati withResponse?: TWithResponse; selectFn?: (res: TWithResponse extends true ? InferResponseByStatus - : Extract, { data: {} }>["data"] + : InferResponseData ) => TSelection; throwOnStatusError?: boolean throwOnError?: boolean | ((error: TError) => boolean) @@ -141,7 +146,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati const mutationKey = [{ method, path }] as const; const mutationFn = async - : Extract, { data: {} }>["data"]> + : InferResponseData> (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & { withResponse?: TLocalWithResponse; throwOnStatusError?: boolean; diff --git a/packages/typed-openapi/tests/integration.types.tstyche.ts b/packages/typed-openapi/tests/integration.types.tstyche.ts index 0f64271..15e7c5b 100644 --- a/packages/typed-openapi/tests/integration.types.tstyche.ts +++ b/packages/typed-openapi/tests/integration.types.tstyche.ts @@ -1,7 +1,7 @@ // Integration test for generated query client using TSTyche // This test ensures the generated client (TS types only, no schema validation) has no inference errors -import { mutationOptions, queryOptions, useMutation } from "@tanstack/react-query"; +import { mutationOptions, queryOptions, useMutation, useQuery } from "@tanstack/react-query"; import { describe, expect, it } from "tstyche"; import { type Endpoints, @@ -438,4 +438,19 @@ describe("Example API Client", () => { queryOptions(query); queryOptions(query.queryOptions); }); + + it("TanstackQueryApiClient useQuery withResponse: undefined (global+local)", async () => { + const tanstack = {} as TanstackQueryApiClient; + + const queryWithSomeResponseUnknownData = tanstack.get("/pet/findByStatus", { query: {status: "available"} }); + const queryHook = useQuery(queryWithSomeResponseUnknownData.queryOptions); + + expect(queryHook.data).type.toBe(); + // ^? + + const secondQuery = tanstack.get("/pet/custom"); + const secondQueryHook = useQuery(secondQuery.queryOptions); + expect(secondQueryHook.data).type.toBe(); + // ^? + }); }); diff --git a/packages/typed-openapi/tests/tanstack-query.generator.test.ts b/packages/typed-openapi/tests/tanstack-query.generator.test.ts index 767d833..f972f8a 100644 --- a/packages/typed-openapi/tests/tanstack-query.generator.test.ts +++ b/packages/typed-openapi/tests/tanstack-query.generator.test.ts @@ -20,6 +20,7 @@ describe("generator", () => { SuccessStatusCode, ErrorStatusCode, InferResponseByStatus, + TypedSuccessResponse, } from "./api.client.ts"; import { errorStatusCodes, TypedResponseError } from "./api.client.ts"; @@ -75,6 +76,11 @@ describe("generator", () => { type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T]; + type InferResponseData = + TypedSuccessResponse extends InferResponseByStatus + ? Extract, { data: {} }>["data"] + : Extract["data"], {}>; + // // @@ -101,7 +107,7 @@ describe("generator", () => { withResponse: false as const, }; const res = await this.client.put(path, requestParams as never); - return res as Extract, { data: {} }>["data"]; + return res as InferResponseData; }, queryKey: queryKey, }), @@ -131,7 +137,7 @@ describe("generator", () => { withResponse: false as const, }; const res = await this.client.post(path, requestParams as never); - return res as Extract, { data: {} }>["data"]; + return res as InferResponseData; }, queryKey: queryKey, }), @@ -161,7 +167,7 @@ describe("generator", () => { withResponse: false as const, }; const res = await this.client.get(path, requestParams as never); - return res as Extract, { data: {} }>["data"]; + return res as InferResponseData; }, queryKey: queryKey, }), @@ -191,7 +197,7 @@ describe("generator", () => { withResponse: false as const, }; const res = await this.client.delete(path, requestParams as never); - return res as Extract, { data: {} }>["data"]; + return res as InferResponseData; }, queryKey: queryKey, }), @@ -213,7 +219,7 @@ describe("generator", () => { TWithResponse extends boolean = false, TSelection = TWithResponse extends true ? InferResponseByStatus - : Extract, { data: {} }>["data"], + : InferResponseData, TError = TEndpoint extends { responses: infer TResponses } ? TResponses extends Record ? InferResponseByStatus @@ -227,7 +233,7 @@ describe("generator", () => { selectFn?: ( res: TWithResponse extends true ? InferResponseByStatus - : Extract, { data: {} }>["data"], + : InferResponseData, ) => TSelection; throwOnStatusError?: boolean; throwOnError?: boolean | ((error: TError) => boolean); @@ -238,7 +244,7 @@ describe("generator", () => { TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true ? InferResponseByStatus - : Extract, { data: {} }>["data"], + : InferResponseData, >( params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & { withResponse?: TLocalWithResponse;