Skip to content

Commit

Permalink
fix(openapi-react-query): correctly infer select return value (#2105)
Browse files Browse the repository at this point in the history
Co-authored-by: Hagen Morano <[email protected]>
  • Loading branch information
HagenMorano and Hagen Morano authored Jan 22, 2025
1 parent 7d6e896 commit af0e72f
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-meals-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-react-query": patch
---

[#1845](https://github.com/openapi-ts/openapi-typescript/pull/2105): The return value of the `select` property is now considered when inferring the `data` type.
42 changes: 35 additions & 7 deletions packages/openapi-react-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
import type { ClientMethod, FetchResponse, MaybeOptionalInit, Client as FetchClient } from "openapi-fetch";
import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers";

// Helper type to dynamically infer the type from the `select` property
type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;

type InitWithUnknowns<Init> = Init & { [key: string]: unknown };

export type QueryKey<
Expand All @@ -29,7 +32,12 @@ export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
Options extends Omit<
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
UseQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<Response["data"], Options["select"]>,
QueryKey<Paths, Method, Path>
>,
"queryKey" | "queryFn"
>,
>(
Expand All @@ -40,11 +48,21 @@ export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod,
: [InitWithUnknowns<Init>, Options?]
) => NoInfer<
Omit<
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
UseQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<Response["data"], Options["select"]>,
QueryKey<Paths, Method, Path>
>,
"queryFn"
> & {
queryFn: Exclude<
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>["queryFn"],
UseQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<Response["data"], Options["select"]>,
QueryKey<Paths, Method, Path>
>["queryFn"],
SkipToken | undefined
>;
}
Expand All @@ -56,7 +74,12 @@ export type UseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
Options extends Omit<
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
UseQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<Response["data"], Options["select"]>,
QueryKey<Paths, Method, Path>
>,
"queryKey" | "queryFn"
>,
>(
Expand All @@ -65,15 +88,20 @@ export type UseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>,
...[init, options, queryClient]: RequiredKeysOf<Init> extends never
? [InitWithUnknowns<Init>?, Options?, QueryClient?]
: [InitWithUnknowns<Init>, Options?, QueryClient?]
) => UseQueryResult<Response["data"], Response["error"]>;
) => UseQueryResult<InferSelectReturnType<Response["data"], Options["select"]>, Response["error"]>;

export type UseSuspenseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
Options extends Omit<
UseSuspenseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
UseSuspenseQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<Response["data"], Options["select"]>,
QueryKey<Paths, Method, Path>
>,
"queryKey" | "queryFn"
>,
>(
Expand All @@ -82,7 +110,7 @@ export type UseSuspenseQueryMethod<Paths extends Record<string, Record<HttpMetho
...[init, options, queryClient]: RequiredKeysOf<Init> extends never
? [InitWithUnknowns<Init>?, Options?, QueryClient?]
: [InitWithUnknowns<Init>, Options?, QueryClient?]
) => UseSuspenseQueryResult<Response["data"], Response["error"]>;
) => UseSuspenseQueryResult<InferSelectReturnType<Response["data"], Options["select"]>, Response["error"]>;

export type UseMutationMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
Method extends HttpMethod,
Expand Down
35 changes: 34 additions & 1 deletion packages/openapi-react-query/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ describe("client", () => {
});

describe("useQuery", () => {
it("should resolve data properly and have error as null when successfull request", async () => {
it("should resolve data properly and have error as null when successful request", async () => {
const response = ["one", "two", "three"];
const fetchClient = createFetchClient<paths>({ baseUrl });
const client = createClient(fetchClient);
Expand Down Expand Up @@ -341,6 +341,39 @@ describe("client", () => {
expectTypeOf(error).toEqualTypeOf<{ code: number; message: string } | null>();
});

it("should infer correct data when used with select property", async () => {
const fetchClient = createFetchClient<paths>({ baseUrl, fetch: fetchInfinite });
const client = createClient(fetchClient);

const { result } = renderHook(
() =>
client.useQuery(
"get",
"/string-array",
{},
{
select: (data) => ({
originalData: data,
customData: 1,
}),
},
),
{
wrapper,
},
);

const { data } = result.current;

expectTypeOf(data).toEqualTypeOf<
| {
originalData: string[];
customData: number;
}
| undefined
>();
});

it("passes abort signal to fetch", async () => {
let signalPassedToFetch: AbortSignal | undefined;

Expand Down

0 comments on commit af0e72f

Please sign in to comment.