Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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