diff --git a/.changeset/query-options-infer.md b/.changeset/query-options-infer.md new file mode 100644 index 000000000..b6accad5c --- /dev/null +++ b/.changeset/query-options-infer.md @@ -0,0 +1,5 @@ +--- +"openapi-react-query": patch +--- + +Fix return type inference for `queryOptions()` when used inside `useQuery` or `useSuspenseQuery`. diff --git a/.changeset/query-options-queryfn.md b/.changeset/query-options-queryfn.md new file mode 100644 index 000000000..6e6c36ac0 --- /dev/null +++ b/.changeset/query-options-queryfn.md @@ -0,0 +1,5 @@ +--- +"openapi-react-query": patch +--- + +Narrow `queryFn` returned by `queryOptions()` to be a function. diff --git a/packages/openapi-react-query/package.json b/packages/openapi-react-query/package.json index fbac7df08..8120896aa 100644 --- a/packages/openapi-react-query/package.json +++ b/packages/openapi-react-query/package.json @@ -79,7 +79,7 @@ "react-error-boundary": "^4.1.2" }, "peerDependencies": { - "@tanstack/react-query": "^5.0.0", + "@tanstack/react-query": "^5.25.0", "openapi-fetch": "workspace:^" } } diff --git a/packages/openapi-react-query/src/index.ts b/packages/openapi-react-query/src/index.ts index 082577536..97d83a683 100644 --- a/packages/openapi-react-query/src/index.ts +++ b/packages/openapi-react-query/src/index.ts @@ -7,6 +7,7 @@ import { type UseSuspenseQueryResult, type QueryClient, type QueryFunctionContext, + type SkipToken, useMutation, useQuery, useSuspenseQuery, @@ -37,7 +38,17 @@ export type QueryOptionsFunction extends never ? [InitWithUnknowns?, Options?] : [InitWithUnknowns, Options?] -) => UseQueryOptions>; +) => NoInfer< + Omit< + UseQueryOptions>, + "queryFn" + > & { + queryFn: Exclude< + UseQueryOptions>["queryFn"], + SkipToken | undefined + >; + } +>; export type UseQueryMethod>, Media extends MediaType> = < Method extends HttpMethod, @@ -121,11 +132,7 @@ export default function createClient useQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient), useSuspenseQuery: (method, path, ...[init, options, queryClient]) => - useSuspenseQuery( - // @ts-expect-error TODO: fix minor type mismatch between useQuery and useSuspenseQuery - queryOptions(method, path, init as InitWithUnknowns, options), - queryClient, - ), + useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient), useMutation: (method, path, options, queryClient) => useMutation( { diff --git a/packages/openapi-react-query/test/index.test.tsx b/packages/openapi-react-query/test/index.test.tsx index 20616bb36..97550d44e 100644 --- a/packages/openapi-react-query/test/index.test.tsx +++ b/packages/openapi-react-query/test/index.test.tsx @@ -4,10 +4,39 @@ import type { paths } from "./fixtures/api.js"; import createClient from "../src/index.js"; import createFetchClient from "openapi-fetch"; import { fireEvent, render, renderHook, screen, waitFor, act } from "@testing-library/react"; -import { QueryClient, QueryClientProvider, useQueries } from "@tanstack/react-query"; +import { + QueryClient, + QueryClientProvider, + useQueries, + useQuery, + useSuspenseQuery, + skipToken, +} from "@tanstack/react-query"; import { Suspense, type ReactNode } from "react"; import { ErrorBoundary } from "react-error-boundary"; +type minimalGetPaths = { + // Without parameters. + "/foo": { + get: { + responses: { + 200: { content: { "application/json": true } }; + 500: { content: { "application/json": false } }; + }; + }; + }; + // With some parameters (makes init required) and different responses. + "/bar": { + get: { + parameters: { query: {} }; + responses: { + 200: { content: { "application/json": "bar 200" } }; + 500: { content: { "application/json": "bar 500" } }; + }; + }; + }; +}; + const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -27,9 +56,7 @@ const fetchInfinite = async () => { beforeAll(() => { server.listen({ - onUnhandledRequest: (request) => { - throw new Error(`No request handler found for ${request.method} ${request.url}`); - }, + onUnhandledRequest: "error", }); }); @@ -96,7 +123,7 @@ describe("client", () => { expect(data).toEqual(response); }); - it("returns query options that can be passed to useQueries and have correct types inferred", async () => { + it("returns query options that can be passed to useQueries", async () => { const fetchClient = createFetchClient({ baseUrl, fetch: fetchInfinite }); const client = createClient(fetchClient); @@ -150,6 +177,60 @@ describe("client", () => { // Generated different queryKey for each query. expect(queryClient.isFetching()).toBe(4); }); + + it("returns query options that can be passed to useQuery", async () => { + const SKIP = { queryKey: [] as any, queryFn: skipToken } as const; + + const fetchClient = createFetchClient({ baseUrl }); + const client = createClient(fetchClient); + + const { result } = renderHook( + () => + useQuery( + // biome-ignore lint/correctness/noConstantCondition: it's just here to test types + false + ? { + ...client.queryOptions("get", "/foo"), + select: (data) => { + expectTypeOf(data).toEqualTypeOf(); + + return "select(true)" as const; + }, + } + : SKIP, + ), + { wrapper }, + ); + + expectTypeOf(result.current.data).toEqualTypeOf<"select(true)" | undefined>(); + expectTypeOf(result.current.error).toEqualTypeOf(); + }); + + it("returns query options that can be passed to useSuspenseQuery", async () => { + const fetchClient = createFetchClient({ + baseUrl, + fetch: () => Promise.resolve(Response.json(true)), + }); + const client = createClient(fetchClient); + + const { result } = renderHook( + () => + useSuspenseQuery({ + ...client.queryOptions("get", "/foo"), + select: (data) => { + expectTypeOf(data).toEqualTypeOf(); + + return "select(true)" as const; + }, + }), + { wrapper }, + ); + + await waitFor(() => expect(result.current).not.toBeNull()); + + expectTypeOf(result.current.data).toEqualTypeOf<"select(true)">(); + expectTypeOf(result.current.error).toEqualTypeOf(); + }); }); describe("useQuery", () => { @@ -203,7 +284,7 @@ describe("client", () => { }); it("should infer correct data and error type", async () => { - const fetchClient = createFetchClient({ baseUrl }); + const fetchClient = createFetchClient({ baseUrl, fetch: fetchInfinite }); const client = createClient(fetchClient); const { result } = renderHook(() => client.useQuery("get", "/string-array"), {