Skip to content

Commit af0e72f

Browse files
HagenMoranoHagen Morano
and
Hagen Morano
authored
fix(openapi-react-query): correctly infer select return value (#2105)
Co-authored-by: Hagen Morano <[email protected]>
1 parent 7d6e896 commit af0e72f

File tree

3 files changed

+74
-8
lines changed

3 files changed

+74
-8
lines changed

.changeset/chilly-meals-act.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-react-query": patch
3+
---
4+
5+
[#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.

packages/openapi-react-query/src/index.ts

+35-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import {
1515
import type { ClientMethod, FetchResponse, MaybeOptionalInit, Client as FetchClient } from "openapi-fetch";
1616
import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers";
1717

18+
// Helper type to dynamically infer the type from the `select` property
19+
type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;
20+
1821
type InitWithUnknowns<Init> = Init & { [key: string]: unknown };
1922

2023
export type QueryKey<
@@ -29,7 +32,12 @@ export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod,
2932
Init extends MaybeOptionalInit<Paths[Path], Method>,
3033
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
3134
Options extends Omit<
32-
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
35+
UseQueryOptions<
36+
Response["data"],
37+
Response["error"],
38+
InferSelectReturnType<Response["data"], Options["select"]>,
39+
QueryKey<Paths, Method, Path>
40+
>,
3341
"queryKey" | "queryFn"
3442
>,
3543
>(
@@ -40,11 +48,21 @@ export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod,
4048
: [InitWithUnknowns<Init>, Options?]
4149
) => NoInfer<
4250
Omit<
43-
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
51+
UseQueryOptions<
52+
Response["data"],
53+
Response["error"],
54+
InferSelectReturnType<Response["data"], Options["select"]>,
55+
QueryKey<Paths, Method, Path>
56+
>,
4457
"queryFn"
4558
> & {
4659
queryFn: Exclude<
47-
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>["queryFn"],
60+
UseQueryOptions<
61+
Response["data"],
62+
Response["error"],
63+
InferSelectReturnType<Response["data"], Options["select"]>,
64+
QueryKey<Paths, Method, Path>
65+
>["queryFn"],
4866
SkipToken | undefined
4967
>;
5068
}
@@ -56,7 +74,12 @@ export type UseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>,
5674
Init extends MaybeOptionalInit<Paths[Path], Method>,
5775
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
5876
Options extends Omit<
59-
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
77+
UseQueryOptions<
78+
Response["data"],
79+
Response["error"],
80+
InferSelectReturnType<Response["data"], Options["select"]>,
81+
QueryKey<Paths, Method, Path>
82+
>,
6083
"queryKey" | "queryFn"
6184
>,
6285
>(
@@ -65,15 +88,20 @@ export type UseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>,
6588
...[init, options, queryClient]: RequiredKeysOf<Init> extends never
6689
? [InitWithUnknowns<Init>?, Options?, QueryClient?]
6790
: [InitWithUnknowns<Init>, Options?, QueryClient?]
68-
) => UseQueryResult<Response["data"], Response["error"]>;
91+
) => UseQueryResult<InferSelectReturnType<Response["data"], Options["select"]>, Response["error"]>;
6992

7093
export type UseSuspenseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
7194
Method extends HttpMethod,
7295
Path extends PathsWithMethod<Paths, Method>,
7396
Init extends MaybeOptionalInit<Paths[Path], Method>,
7497
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
7598
Options extends Omit<
76-
UseSuspenseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
99+
UseSuspenseQueryOptions<
100+
Response["data"],
101+
Response["error"],
102+
InferSelectReturnType<Response["data"], Options["select"]>,
103+
QueryKey<Paths, Method, Path>
104+
>,
77105
"queryKey" | "queryFn"
78106
>,
79107
>(
@@ -82,7 +110,7 @@ export type UseSuspenseQueryMethod<Paths extends Record<string, Record<HttpMetho
82110
...[init, options, queryClient]: RequiredKeysOf<Init> extends never
83111
? [InitWithUnknowns<Init>?, Options?, QueryClient?]
84112
: [InitWithUnknowns<Init>, Options?, QueryClient?]
85-
) => UseSuspenseQueryResult<Response["data"], Response["error"]>;
113+
) => UseSuspenseQueryResult<InferSelectReturnType<Response["data"], Options["select"]>, Response["error"]>;
86114

87115
export type UseMutationMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
88116
Method extends HttpMethod,

packages/openapi-react-query/test/index.test.tsx

+34-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ describe("client", () => {
234234
});
235235

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

344+
it("should infer correct data when used with select property", async () => {
345+
const fetchClient = createFetchClient<paths>({ baseUrl, fetch: fetchInfinite });
346+
const client = createClient(fetchClient);
347+
348+
const { result } = renderHook(
349+
() =>
350+
client.useQuery(
351+
"get",
352+
"/string-array",
353+
{},
354+
{
355+
select: (data) => ({
356+
originalData: data,
357+
customData: 1,
358+
}),
359+
},
360+
),
361+
{
362+
wrapper,
363+
},
364+
);
365+
366+
const { data } = result.current;
367+
368+
expectTypeOf(data).toEqualTypeOf<
369+
| {
370+
originalData: string[];
371+
customData: number;
372+
}
373+
| undefined
374+
>();
375+
});
376+
344377
it("passes abort signal to fetch", async () => {
345378
let signalPassedToFetch: AbortSignal | undefined;
346379

0 commit comments

Comments
 (0)