Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/framework/react/guides/parallel-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,8 @@ function App({ users }) {
```

[//]: # 'Example2'
[//]: # 'TypeScriptSelect'

> When using TypeScript, an inline `select` written on a query object passed to `useQueries` can't infer its `data` argument from that same object's `queryFn` — it falls back to `unknown`. Annotate the `select` parameter explicitly, or define the query with the [`queryOptions`](../reference/queryOptions.md) helper, to keep type inference. See [this known limitation](https://github.com/TanStack/query/issues/6556).

[//]: # 'TypeScriptSelect'
78 changes: 78 additions & 0 deletions docs/framework/react/reference/useQueries.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,81 @@ The `combine` function will only re-run if:
- any of the query results changed

This means that an inlined `combine` function, as shown above, will run on every render. To avoid this, you can wrap the `combine` function in `useCallback`, or extract it to a stable function reference if it doesn't have any dependencies.

## TypeScript: typing the `select` option

Unlike `useQuery`, `useQueries` cannot infer the `data` argument of an _inline_ `select` from its sibling `queryFn`. Because `useQueries` infers the type of the whole `queries` array at once, the `select` parameter of a query object written inline cannot be contextually typed from that same object's `queryFn`, so it falls back to `unknown`. This is a [known TypeScript limitation](https://github.com/TanStack/query/issues/6556).

```tsx
useQueries({
queries: [
{
queryKey: ['post', 1],
queryFn: () => fetchPost(1),
// ❌ `data` is `unknown` here
select: (data) => data.title,
},
],
})
```

There are two supported workarounds:

1. Annotate the `select` parameter explicitly:

```tsx
useQueries({
queries: [
{
queryKey: ['post', 1],
queryFn: () => fetchPost(1),
// ✅ `data` is `Post`
select: (data: Post) => data.title,
},
],
})
```

2. Define the query with the [`queryOptions`](./queryOptions.md) helper, which resolves its types in a single object _before_ it reaches `useQueries`:

```tsx
const postOptions = (id: number) =>
queryOptions({
queryKey: ['post', id],
queryFn: () => fetchPost(id),
// ✅ `data` is `Post`
select: (data) => data.title,
})

useQueries({ queries: [postOptions(1), postOptions(2)] })
```

The same limitation applies when you spread a `queryOptions` result to override its `select` inline — the overriding `select` still falls back to `unknown`:

```tsx
useQueries({
queries: [
{
...postOptions(1),
// ❌ `data` is `unknown` here
select: (data) => data.title,
},
],
})
```

Wrap the spread in `queryOptions` again so the override is resolved before it reaches `useQueries`:

```tsx
useQueries({
queries: [
queryOptions({
...postOptions(1),
// ✅ `data` is `Post`
select: (data) => data.title,
}),
],
})
```

The same applies to [`useSuspenseQueries`](./useSuspenseQueries.md).
2 changes: 2 additions & 0 deletions docs/framework/react/reference/useSuspenseQueries.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ The same as for [useQueries](./useQueries.md), except that each `query` can't ha
- `enabled`
- `placeholderData`

> The [`select` typing caveat](./useQueries.md#typescript-typing-the-select-option) for `useQueries` applies here as well: annotate the `select` parameter or use the [`queryOptions`](./queryOptions.md) helper to keep type inference.

**Returns**

Same structure as [useQueries](./useQueries.md), except that for each `query`:
Expand Down
169 changes: 169 additions & 0 deletions packages/react-query/src/__tests__/useQueries.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -868,4 +868,173 @@ describe('useQueries', () => {
}
})
})

describe('select', () => {
// Inferring the `select` argument of an *inline* query object from its
// sibling `queryFn` is a known TypeScript limitation, because `useQueries`
// infers its array generic from the argument itself. The two supported
// workarounds are to annotate the `select` parameter, or to define the
// query with the `queryOptions` helper.
// https://github.com/TanStack/query/issues/6556

describe('without queryOptions (inline query object)', () => {
it('leaves the select argument as `unknown` without an annotation', () => {
useQueries({
queries: [
{
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
select: (data) => {
expectTypeOf(data).toBeUnknown()
// @ts-expect-error `data` is `unknown`, not the expected `number`
return data.toFixed()
},
},
],
})
})

it('infers the result when the select parameter is annotated', () => {
const queryResults = useQueries({
queries: [
{
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
select: (data: number) => data.toFixed(),
},
],
})
expectTypeOf(queryResults[0].data).toEqualTypeOf<string | undefined>()
})
})

describe('with queryOptions passed directly', () => {
it('without select, infers the queryFn data as the result', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
})
const queryResults = useQueries({ queries: [options] })
expectTypeOf(queryResults[0].data).toEqualTypeOf<number | undefined>()
})

it('with select, infers the select argument and the result', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
select: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
return data.toFixed()
},
})
const queryResults = useQueries({ queries: [options] })
expectTypeOf(queryResults[0].data).toEqualTypeOf<string | undefined>()
})

it('infers select when a base queryOptions is re-wrapped with queryOptions', () => {
const baseOptions = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
})
const queryResults = useQueries({
queries: [
queryOptions({
...baseOptions,
select: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
return data.toFixed()
},
}),
baseOptions,
],
})
expectTypeOf(queryResults[0].data).toEqualTypeOf<string | undefined>()
expectTypeOf(queryResults[1].data).toEqualTypeOf<number | undefined>()
})

it('infers an overriding select when a queryOptions with a select is re-wrapped with queryOptions', () => {
const baseOptions = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
select: (data) => data + 1,
})
const queryResults = useQueries({
queries: [
queryOptions({
...baseOptions,
select: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
return data.toFixed()
},
}),
],
})
expectTypeOf(queryResults[0].data).toEqualTypeOf<string | undefined>()
})
})

describe('with queryOptions spread into an inline query object', () => {
it('without select in the factory, leaves an unannotated select as `unknown`', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
})
useQueries({
queries: [
// @ts-expect-error Without an annotation the inline `select` receives `data: unknown`, which makes the whole spread query object unassignable to the expected options type
{
...options,
select: (data) => {
expectTypeOf(data).toBeUnknown()
return data
},
},
],
})
})

it('without select in the factory, an annotated select compiles', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
})
const queryResults = useQueries({
queries: [{ ...options, select: (data: number) => data.toFixed() }],
})
expectTypeOf(queryResults[0].data).toEqualTypeOf<string | undefined>()
})

it('with select in the factory, leaves an unannotated overriding select as `unknown`', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
select: (data) => data + 1,
})
useQueries({
queries: [
// @ts-expect-error Without an annotation the inline `select` receives `data: unknown`, which makes the whole spread query object unassignable to the expected options type
{
...options,
select: (data) => {
expectTypeOf(data).toBeUnknown()
return data
},
},
],
})
})

it('with select in the factory, an annotated overriding select compiles', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
select: (data) => data + 1,
})
const queryResults = useQueries({
queries: [{ ...options, select: (data: number) => data.toFixed() }],
})
expectTypeOf(queryResults[0].data).toEqualTypeOf<string | undefined>()
})
})
})
})
Loading
Loading