Skip to content

Commit d45dfd5

Browse files
authored
Merge pull request #58 from dev-five-git/fix-queries-type
Fix useQueries type
2 parents 7538d24 + 2180f8f commit d45dfd5

File tree

6 files changed

+290
-35
lines changed

6 files changed

+290
-35
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"packages/react-query/package.json":"Patch"},"note":"Fix useQueries type","date":"2026-03-25T11:48:18.986238800Z"}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3535

3636
.claude
3737
.sisyphus
38+
.omc

bun.lock

Lines changed: 9 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"private": true,
66
"devDependencies": {
77
"@biomejs/biome": "^2.3",
8+
"@devup-api/react-query": "workspace:*",
89
"@testing-library/react": "^16.3.2",
910
"@testing-library/react-hooks": "^8.0.1",
1011
"@types/bun": "latest",
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* Type tests for DevupQueryClient
3+
* Verify that useQueries type inference works correctly per element
4+
*/
5+
import { describe, expectTypeOf, test } from 'bun:test'
6+
import type { DevupGetApiStruct } from '@devup-api/core'
7+
import type { DevupQueryClient } from '../query-client'
8+
9+
// =============================================================================
10+
// Test Fixtures
11+
// =============================================================================
12+
13+
declare module '@devup-api/core' {
14+
interface DevupApiServers {
15+
'react-query-test.json': never
16+
}
17+
18+
interface DevupGetApiStruct {
19+
'react-query-test.json': {
20+
'/users': {
21+
response: { id: number; name: string }[]
22+
error: { message: string }
23+
}
24+
'/users/{id}': {
25+
params: { id: string }
26+
response: { id: number; name: string; email: string }
27+
error: { message: string; code: number }
28+
}
29+
'/posts': {
30+
response: { id: number; title: string }[]
31+
error: { message: string }
32+
}
33+
}
34+
}
35+
36+
interface DevupPostApiStruct {
37+
'react-query-test.json': {
38+
'/users': {
39+
body: { name: string; email: string }
40+
response: { id: number }
41+
error: { message: string }
42+
}
43+
}
44+
}
45+
46+
interface DevupDeleteApiStruct {
47+
'react-query-test.json': {
48+
'/users/{id}': {
49+
params: { id: string }
50+
response: { success: boolean }
51+
error: { message: string }
52+
}
53+
}
54+
}
55+
}
56+
57+
type QC = DevupQueryClient<'react-query-test.json'>
58+
59+
// =============================================================================
60+
// useQueries - Per-element return type inference
61+
// =============================================================================
62+
63+
describe('useQueries per-element type inference', () => {
64+
test('different endpoints return different response types', () => {
65+
type Result = ReturnType<
66+
(
67+
qc: QC,
68+
) => ReturnType<
69+
typeof qc.useQueries<[['get', '/users'], ['get', '/posts']]>
70+
>
71+
>
72+
73+
expectTypeOf<Result[0]['data']>().toEqualTypeOf<
74+
{ id: number; name: string }[] | undefined
75+
>()
76+
expectTypeOf<Result[1]['data']>().toEqualTypeOf<
77+
{ id: number; title: string }[] | undefined
78+
>()
79+
})
80+
81+
test('different endpoints return different error types', () => {
82+
type Result = ReturnType<
83+
(
84+
qc: QC,
85+
) => ReturnType<
86+
typeof qc.useQueries<
87+
[
88+
['get', '/users/{id}', { params: { id: string } }],
89+
['get', '/users'],
90+
]
91+
>
92+
>
93+
>
94+
95+
expectTypeOf<Result[0]['data']>().toEqualTypeOf<
96+
{ id: number; name: string; email: string } | undefined
97+
>()
98+
expectTypeOf<Result[0]['error']>().toEqualTypeOf<{
99+
message: string
100+
code: number
101+
} | null>()
102+
expectTypeOf<Result[1]['data']>().toEqualTypeOf<
103+
{ id: number; name: string }[] | undefined
104+
>()
105+
expectTypeOf<Result[1]['error']>().toEqualTypeOf<{
106+
message: string
107+
} | null>()
108+
})
109+
110+
test('single element preserves exact type', () => {
111+
type Result = ReturnType<
112+
(qc: QC) => ReturnType<typeof qc.useQueries<[['get', '/posts']]>>
113+
>
114+
115+
expectTypeOf<Result[0]['data']>().toEqualTypeOf<
116+
{ id: number; title: string }[] | undefined
117+
>()
118+
})
119+
120+
test('result types are not intersected', () => {
121+
type Result = ReturnType<
122+
(
123+
qc: QC,
124+
) => ReturnType<
125+
typeof qc.useQueries<[['get', '/users'], ['get', '/posts']]>
126+
>
127+
>
128+
129+
// result[0] should NOT have title (from /posts)
130+
type Data0 = NonNullable<Result[0]['data']>[number]
131+
expectTypeOf<
132+
'title' extends keyof Data0 ? true : false
133+
>().toEqualTypeOf<false>()
134+
135+
// result[1] should NOT have name (from /users)
136+
type Data1 = NonNullable<Result[1]['data']>[number]
137+
expectTypeOf<
138+
'name' extends keyof Data1 ? true : false
139+
>().toEqualTypeOf<false>()
140+
})
141+
})
142+
143+
// =============================================================================
144+
// useQueries - params constraint
145+
// =============================================================================
146+
147+
describe('useQueries params constraint', () => {
148+
test('endpoint with params accepts params option', () => {
149+
// This should be valid: correct params provided
150+
type _Valid = ReturnType<
151+
(
152+
qc: QC,
153+
) => ReturnType<
154+
typeof qc.useQueries<
155+
[['get', '/users/{id}', { params: { id: string } }]]
156+
>
157+
>
158+
>
159+
})
160+
161+
test('endpoint without params has optional options', () => {
162+
// This should be valid: no options needed
163+
type _Valid = ReturnType<
164+
(qc: QC) => ReturnType<typeof qc.useQueries<[['get', '/users']]>>
165+
>
166+
})
167+
})
168+
169+
// =============================================================================
170+
// useQuery - single query type inference (baseline)
171+
// =============================================================================
172+
173+
describe('useQuery type inference baseline', () => {
174+
test('response type inferred from endpoint', () => {
175+
type Result = ReturnType<
176+
(
177+
qc: QC,
178+
) => ReturnType<
179+
typeof qc.useQuery<
180+
'get',
181+
DevupGetApiStruct['react-query-test.json'],
182+
'/users'
183+
>
184+
>
185+
>
186+
187+
expectTypeOf<Result['data']>().toEqualTypeOf<
188+
{ id: number; name: string }[] | undefined
189+
>()
190+
})
191+
192+
test('error type inferred from endpoint', () => {
193+
type Result = ReturnType<
194+
(
195+
qc: QC,
196+
) => ReturnType<
197+
typeof qc.useQuery<
198+
'get',
199+
DevupGetApiStruct['react-query-test.json'],
200+
'/users/{id}'
201+
>
202+
>
203+
>
204+
205+
expectTypeOf<Result['data']>().toEqualTypeOf<
206+
{ id: number; name: string; email: string } | undefined
207+
>()
208+
expectTypeOf<Result['error']>().toEqualTypeOf<{
209+
message: string
210+
code: number
211+
} | null>()
212+
})
213+
})

0 commit comments

Comments
 (0)