Skip to content

Commit 8ce6877

Browse files
committed
fix: make TuyauPromise implement Promise<T> for TanStack Query compatibility
TuyauPromise only implemented PromiseLike<T> and was missing [Symbol.toStringTag], so TypeScript didn't consider it assignable to Promise<T>. This caused TanStack Query to infer data as TuyauPromise<Response> instead of Response when using @tuyau/core directly with useQuery.
1 parent 49547c4 commit 8ce6877

File tree

4 files changed

+56
-2
lines changed

4 files changed

+56
-2
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@tuyau/core': patch
3+
---
4+
5+
Fix `TuyauPromise` not being assignable to `Promise<T>`, which broke TanStack Query type inference.
6+
7+
`TuyauPromise` implemented `PromiseLike<T>` but was missing `[Symbol.toStringTag]`, so TypeScript did not consider it structurally compatible with `Promise<T>`. When users passed a tuyau call directly to TanStack Query's `queryFn`, the `data` type was inferred as `TuyauPromise<Response>` instead of `Response`:
8+
9+
```ts
10+
// Before: data was typed as TuyauPromise<User[]> instead of User[]
11+
const { data } = useQuery({
12+
queryKey: ['users'],
13+
queryFn: () => tuyau.request('users.index', {}),
14+
})
15+
```
16+
17+
`TuyauPromise` now implements the full `Promise<T>` interface, so it works seamlessly with TanStack Query and any other library expecting `Promise<T>`.

packages/core/src/client/promise.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ type NormalizeError<E> =
1212

1313
/**
1414
* A promise wrapper that adds `.safe()` for non-throwing error handling.
15-
* Implements PromiseLike so `await` works transparently.
15+
* Implements Promise so it's fully compatible with TanStack Query and other
16+
* libraries that expect `Promise<T>` return types.
1617
*/
17-
export class TuyauPromise<Data, Errors = unknown> implements PromiseLike<Data> {
18+
export class TuyauPromise<Data, Errors = unknown> implements Promise<Data> {
19+
readonly [Symbol.toStringTag]: string = 'TuyauPromise'
1820
#promise: Promise<Data>
1921

2022
constructor(promise: Promise<Data>) {

packages/core/tests/typings.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,16 @@ test.group('Client | Typings', (group) => {
473473
expectTypeOf<DownloadsParams>().toEqualTypeOf<{ '*': string[] }>()
474474
})
475475

476+
test('TuyauPromise is assignable to Promise', ({ expectTypeOf }) => {
477+
const tuyau = createTuyau({ baseUrl: 'http://localhost:3333', registry })
478+
479+
const result = tuyau.get('/users', {})
480+
481+
// This is the scenario that breaks TanStack Query:
482+
// queryFn expects Promise<T>, but TuyauPromise only implements PromiseLike<T>
483+
expectTypeOf(result).toMatchTypeOf<Promise<{ token: string }>>()
484+
})
485+
476486
test('safe() returns TuyauPromise with correct error types', ({ expectTypeOf }) => {
477487
const tuyau = createTuyau({ baseUrl: 'http://localhost:3333', registry })
478488

packages/react-query/tests/query.spec.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,31 @@ test.group('Types | queryOptions useSuspenseQuery compatibility', () => {
250250
})
251251
})
252252

253+
test.group('Types | TuyauPromise assignable to Promise (TanStack Query compat)', () => {
254+
test('useQuery with raw tuyau client should infer response type, not TuyauPromise', ({
255+
expectTypeOf,
256+
}) => {
257+
const client = createTuyau({ baseUrl: 'http://localhost:3333', registry: defaultRegistry })
258+
259+
// When users use @tuyau/core directly with TanStack Query (without @tuyau/react-query),
260+
// queryFn returns TuyauPromise which implements PromiseLike but NOT Promise.
261+
// TanStack Query's queryFn is typed as `() => T | Promise<T>`.
262+
// Since TuyauPromise is not a Promise, TS resolves T = TuyauPromise<Response>
263+
// instead of unwrapping it to T = Response. This makes `data` wrongly typed.
264+
const { result } = renderHookWithWrapper(() =>
265+
useQuery({
266+
queryKey: ['users'] as const,
267+
queryFn: () => client.request('users.index', {}),
268+
}),
269+
)
270+
271+
// BUG: data is TuyauPromise<Array<...>> | undefined instead of Array<...> | undefined
272+
expectTypeOf(result.current.data).toEqualTypeOf<
273+
Array<{ id: number; name: string }> | undefined
274+
>()
275+
})
276+
})
277+
253278
test.group('Query | Filters', () => {
254279
test('queryFilter should merge filters with queryKey', ({ assert }) => {
255280
const client = createTuyau({ baseUrl: 'http://localhost:3333', registry: defaultRegistry })

0 commit comments

Comments
 (0)