From 7e8b4b0b3374ef627dbe8728a6e5d871017945ed Mon Sep 17 00:00:00 2001 From: hugiex Date: Mon, 23 Feb 2026 13:24:15 +0700 Subject: [PATCH 1/7] fix(query-db-collection): align queryOptions interop types --- docs/collections/query-collection.md | 31 ++++ packages/query-db-collection/src/query.ts | 146 +++++++++++++-- .../query-db-collection/tests/query.test-d.ts | 174 +++++++++++++++--- 3 files changed, 305 insertions(+), 46 deletions(-) diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index cf13291a8..4302da516 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -64,6 +64,37 @@ The `queryCollectionOptions` function accepts the following options: - `staleTime`: How long data is considered fresh - `meta`: Optional metadata that will be passed to the query function context +### Interop with `queryOptions(...)` + +If your app already uses a TanStack Query options helper (for example, `queryOptions` from `@tanstack/react-query`), you can pass those options directly into `queryCollectionOptions`: + +```typescript +import { QueryClient } from "@tanstack/query-core" +import { createCollection } from "@tanstack/db" +import { queryCollectionOptions } from "@tanstack/query-db-collection" +import { queryOptions } from "@tanstack/react-query" + +const queryClient = new QueryClient() + +const listOptions = queryOptions({ + queryKey: ["todos"], + queryFn: async () => { + const response = await fetch("/api/todos") + return response.json() as Promise> + }, +}) + +const todosCollection = createCollection( + queryCollectionOptions({ + ...listOptions, + queryClient, + getKey: (item) => item.id, + }), +) +``` + +`queryFn` is still required at runtime for query collections. If it is missing, `queryCollectionOptions` throws `QueryFnRequiredError`. + ### Collection Options - `id`: Unique identifier for the collection diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 920cdb69a..e1becdefb 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -20,6 +20,7 @@ import type { UtilsRecord, } from '@tanstack/db' import type { + DataTag, FetchStatus, QueryClient, QueryFunctionContext, @@ -47,6 +48,39 @@ type InferSchemaInput = T extends StandardSchemaV1 : Record type TQueryKeyBuilder = (opts: LoadSubsetOptions) => TQueryKey +type TaggedQueryKey = DataTag< + TQueryKey, + TQueryData, + TError +> +type QueryOptionsInteropConfig< + T extends object, + TError, + TQueryKey extends QueryKey, + TQueryData, + TKey extends string | number = string | number, + TSchema extends StandardSchemaV1 = never, +> = Omit< + QueryCollectionConfig< + T, + ( + context: QueryFunctionContext< + TaggedQueryKey + >, + ) => Promise, + TError, + TaggedQueryKey, + TKey, + TSchema, + TQueryData + >, + `queryFn` | `queryKey` +> & { + queryKey: TaggedQueryKey + queryFn?: ( + context: QueryFunctionContext>, + ) => TQueryData | Promise +} /** * Configuration options for creating a Query Collection @@ -59,9 +93,9 @@ type TQueryKeyBuilder = (opts: LoadSubsetOptions) => TQueryKey */ export interface QueryCollectionConfig< T extends object = object, - TQueryFn extends (context: QueryFunctionContext) => Promise = ( + TQueryFn extends (context: QueryFunctionContext) => any = ( context: QueryFunctionContext, - ) => Promise, + ) => any, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, @@ -73,8 +107,10 @@ export interface QueryCollectionConfig< /** Function that fetches data from the server. Must return the complete collection state */ queryFn: TQueryFn extends ( context: QueryFunctionContext, - ) => Promise> - ? (context: QueryFunctionContext) => Promise> + ) => Promise> | Array + ? ( + context: QueryFunctionContext, + ) => Promise> | Array : TQueryFn /* Function that extracts array items from wrapped API responses (e.g metadata, pagination) */ select?: (data: TQueryData) => Array @@ -83,33 +119,39 @@ export interface QueryCollectionConfig< // Query-specific options /** Whether the query should automatically run (default: true) */ - enabled?: boolean - refetchInterval?: QueryObserverOptions< - Array, + enabled?: QueryObserverOptions< + TQueryData, TError, Array, + TQueryData, + TQueryKey + >[`enabled`] + refetchInterval?: QueryObserverOptions< + TQueryData, + TError, Array, + TQueryData, TQueryKey >[`refetchInterval`] retry?: QueryObserverOptions< - Array, + TQueryData, TError, Array, - Array, + TQueryData, TQueryKey >[`retry`] retryDelay?: QueryObserverOptions< - Array, + TQueryData, TError, Array, - Array, + TQueryData, TQueryKey >[`retryDelay`] staleTime?: QueryObserverOptions< - Array, + TQueryData, TError, Array, - Array, + TQueryData, TQueryKey >[`staleTime`] @@ -393,7 +435,7 @@ class QueryCollectionUtilsImpl { // Overload for when schema is provided and select present export function queryCollectionOptions< T extends StandardSchemaV1, - TQueryFn extends (context: QueryFunctionContext) => Promise, + TQueryFn extends (context: QueryFunctionContext) => any, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, @@ -428,9 +470,9 @@ export function queryCollectionOptions< // Overload for when no schema is provided and select present export function queryCollectionOptions< T extends object, - TQueryFn extends (context: QueryFunctionContext) => Promise = ( + TQueryFn extends (context: QueryFunctionContext) => any = ( context: QueryFunctionContext, - ) => Promise, + ) => any, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, @@ -458,6 +500,35 @@ export function queryCollectionOptions< utils: QueryCollectionUtils } +// Interop overload for queryOptions(...) + select (no schema) +export function queryCollectionOptions< + T extends object, + TError = unknown, + TQueryKey extends QueryKey = QueryKey, + TKey extends string | number = string | number, + TQueryData = unknown, +>( + config: QueryOptionsInteropConfig< + T, + TError, + TQueryKey, + TQueryData, + TKey, + never + > & { + schema?: never // prohibit schema + select: (data: TQueryData) => Array + }, +): CollectionConfig< + T, + TKey, + never, + QueryCollectionUtils +> & { + schema?: never // no schema in the result + utils: QueryCollectionUtils +} + // Overload for when schema is provided export function queryCollectionOptions< T extends StandardSchemaV1, @@ -469,7 +540,7 @@ export function queryCollectionOptions< InferSchemaOutput, ( context: QueryFunctionContext, - ) => Promise>>, + ) => Array> | Promise>>, TError, TQueryKey, TKey, @@ -501,7 +572,7 @@ export function queryCollectionOptions< >( config: QueryCollectionConfig< T, - (context: QueryFunctionContext) => Promise>, + (context: QueryFunctionContext) => Array | Promise>, TError, TQueryKey, TKey @@ -518,8 +589,43 @@ export function queryCollectionOptions< utils: QueryCollectionUtils } +// Interop overload for queryOptions(...) (no schema, no select) +export function queryCollectionOptions< + T extends object, + TError = unknown, + TQueryKey extends QueryKey = QueryKey, + TKey extends string | number = string | number, +>( + config: QueryOptionsInteropConfig< + T, + TError, + TQueryKey, + Array, + TKey, + never + > & { + schema?: never // prohibit schema + }, +): CollectionConfig< + T, + TKey, + never, + QueryCollectionUtils +> & { + schema?: never // no schema in the result + utils: QueryCollectionUtils +} + export function queryCollectionOptions( - config: QueryCollectionConfig>, + config: Omit< + QueryCollectionConfig< + Record, + (context: QueryFunctionContext) => any + >, + `queryFn` + > & { + queryFn?: (context: QueryFunctionContext) => any + }, ): CollectionConfig< Record, string | number, @@ -555,7 +661,7 @@ export function queryCollectionOptions( if (!queryKey) { throw new QueryKeyRequiredError() } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!queryFn) { throw new QueryFnRequiredError() } diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index 4f53e2c85..a7130b185 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -10,6 +10,7 @@ import { import { QueryClient } from '@tanstack/query-core' import { z } from 'zod' import { queryCollectionOptions } from '../src/query' +import type { DataTag, QueryObserverOptions } from '@tanstack/query-core' import type { QueryCollectionConfig, QueryCollectionUtils } from '../src/query' import type { DeleteMutationFnParams, @@ -223,7 +224,7 @@ describe(`Query collection type resolution tests`, () => { queryFn: async (): Promise> => { return [] as Array }, - getKey: (item) => item.id, + getKey: (item: TodoType) => item.id, }) // Should infer TodoType from queryFn @@ -236,15 +237,17 @@ describe(`Query collection type resolution tests`, () => { name: string } - queryCollectionOptions({ + const invalidConfig = { queryClient, queryKey: [`explicit-priority`], - // @ts-expect-error – queryFn doesn't match the explicit type queryFn: async (): Promise> => { return [] as Array }, - getKey: (item) => item.id, - }) + getKey: (item: UserType) => item.id, + } + + // @ts-expect-error – queryFn doesn't match the explicit type + queryCollectionOptions(invalidConfig) }) it(`should prioritize schema over queryFn`, () => { @@ -281,20 +284,18 @@ describe(`Query collection type resolution tests`, () => { email: z.string(), }) - const options = queryCollectionOptions({ + const invalidConfig = { queryClient, queryKey: [`schema-priority`], queryFn: async () => { return [] as Array }, - // @ts-expect-error – queryFn doesn't match the schema type schema: userSchema, - getKey: (item) => item.id, - }) + getKey: (item: z.infer) => item.id, + } - // Should use schema type, not TodoType from queryFn - type ExpectedType = z.infer - expectTypeOf(options.getKey).parameters.toEqualTypeOf<[ExpectedType]>() + // @ts-expect-error – queryFn doesn't match the schema type + queryCollectionOptions(invalidConfig) }) it(`should maintain backward compatibility with explicit types`, () => { @@ -346,34 +347,31 @@ describe(`Query collection type resolution tests`, () => { }) it(`should error when queryFn returns wrapped data without select`, () => { - const userData = z.object({ - id: z.string(), - name: z.string(), - email: z.string(), - }) - - type UserDataType = z.infer + type UserDataType = { + id: string + name: string + email: string + } type WrappedResponse = { metadata: string data: Array } - queryCollectionOptions({ + const invalidConfig = { queryClient, queryKey: [`wrapped-no-select`], - // @ts-expect-error - queryFn returns wrapped data but no select provided queryFn: (): Promise => { return Promise.resolve({ metadata: `example`, data: [], }) }, - // @ts-expect-error - schema type conflicts with queryFn return type - schema: userData, - // @ts-expect-error - item type is inferred as object due to type mismatch - getKey: (item) => item.id, - }) + getKey: () => `1`, + } + + // @ts-expect-error - queryFn returns wrapped data but no select provided + queryCollectionOptions(invalidConfig) }) it(`select properly extracts array from wrapped response`, () => { @@ -561,6 +559,130 @@ describe(`Query collection type resolution tests`, () => { }) }) + describe(`queryOptions interoperability`, () => { + type NumberItem = { + id: number + value: string + } + type TaggedNumbersKey = DataTag, Array, Error> + type NumberQueryObserverOptions = QueryObserverOptions< + Array, + Error, + Array, + Array, + TaggedNumbersKey + > + const taggedNumbersQueryKey = + [`query-options-numbers`] as unknown as TaggedNumbersKey + + it(`should accept queryOptions-like spread config with tagged queryKey`, () => { + const queryOptionsLike = { + queryKey: taggedNumbersQueryKey, + queryFn: () => + Promise.resolve([ + { id: 1, value: `one` }, + { id: 2, value: `two` }, + ]), + } satisfies { + queryKey: TaggedNumbersKey + queryFn?: NumberQueryObserverOptions[`queryFn`] + } + + const options = queryCollectionOptions({ + ...queryOptionsLike, + queryClient, + getKey: (item) => item.id, + }) + + expectTypeOf(options.getKey).parameters.toEqualTypeOf<[NumberItem]>() + }) + + it(`should accept enabled from queryOptions-like config`, () => { + const queryOptionsLike = { + queryKey: taggedNumbersQueryKey, + queryFn: () => Promise.resolve([{ id: 1, value: `one` }]), + enabled: (_query) => true, + } satisfies { + queryKey: TaggedNumbersKey + queryFn?: NumberQueryObserverOptions[`queryFn`] + enabled?: NumberQueryObserverOptions[`enabled`] + } + + const options = queryCollectionOptions({ + ...queryOptionsLike, + queryClient, + getKey: (item) => item.id, + }) + + expectTypeOf(options.getKey).parameters.toEqualTypeOf<[NumberItem]>() + }) + + it(`should require select for wrapped queryOptions-like responses`, () => { + type WrappedResponse = { + total: number + items: Array + } + type TaggedWrappedKey = DataTag, WrappedResponse, Error> + type WrappedObserverOptions = QueryObserverOptions< + WrappedResponse, + Error, + WrappedResponse, + WrappedResponse, + TaggedWrappedKey + > + const taggedWrappedQueryKey = + [`query-options-wrapped`] as unknown as TaggedWrappedKey + + const wrappedQueryOptionsLike = { + queryKey: taggedWrappedQueryKey, + queryFn: () => + Promise.resolve({ + total: 1, + items: [{ id: 1, value: `one` }], + }), + } satisfies { + queryKey: TaggedWrappedKey + queryFn?: WrappedObserverOptions[`queryFn`] + } + + // @ts-expect-error - wrapped response requires select to extract the item array + queryCollectionOptions({ + ...wrappedQueryOptionsLike, + queryClient, + getKey: () => 1, + }) + + const options = queryCollectionOptions({ + ...wrappedQueryOptionsLike, + select: (response) => response.items, + queryClient, + getKey: (item) => item.id, + }) + + expectTypeOf(options.getKey).parameters.toEqualTypeOf<[NumberItem]>() + }) + + it(`should still require queryFn for plain configs`, () => { + queryCollectionOptions({ + queryClient, + // @ts-expect-error - queryFn is required for plain (non-queryOptions-like) configs + queryKey: [`query-options-missing-query-fn`], + getKey: (item) => item.id, + }) + }) + + it(`should accept synchronous queryFn return values`, () => { + const options = queryCollectionOptions({ + queryClient, + queryKey: [`query-options-sync-query-fn`], + queryFn: () => [{ id: 1, value: `one` }], + getKey: (item) => item.id, + }) + + expectTypeOf(options.getKey).parameters.toEqualTypeOf<[NumberItem]>() + }) + }) + it(`should type collection.utils as QueryCollectionUtils after createCollection`, () => { const collection = createCollection( queryCollectionOptions({ From 5713dbf9ee0df691f36491a90a5bc17ff1643f38 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 06:33:58 +0000 Subject: [PATCH 2/7] ci: apply automated fixes --- packages/query-db-collection/src/query.ts | 10 +++++----- packages/query-db-collection/tests/query.test-d.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index e1becdefb..0b302d6d7 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -78,7 +78,9 @@ type QueryOptionsInteropConfig< > & { queryKey: TaggedQueryKey queryFn?: ( - context: QueryFunctionContext>, + context: QueryFunctionContext< + TaggedQueryKey + >, ) => TQueryData | Promise } @@ -108,9 +110,7 @@ export interface QueryCollectionConfig< queryFn: TQueryFn extends ( context: QueryFunctionContext, ) => Promise> | Array - ? ( - context: QueryFunctionContext, - ) => Promise> | Array + ? (context: QueryFunctionContext) => Promise> | Array : TQueryFn /* Function that extracts array items from wrapped API responses (e.g metadata, pagination) */ select?: (data: TQueryData) => Array @@ -661,7 +661,7 @@ export function queryCollectionOptions( if (!queryKey) { throw new QueryKeyRequiredError() } - + if (!queryFn) { throw new QueryFnRequiredError() } diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index a7130b185..2d384bbac 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -572,8 +572,9 @@ describe(`Query collection type resolution tests`, () => { Array, TaggedNumbersKey > - const taggedNumbersQueryKey = - [`query-options-numbers`] as unknown as TaggedNumbersKey + const taggedNumbersQueryKey = [ + `query-options-numbers`, + ] as unknown as TaggedNumbersKey it(`should accept queryOptions-like spread config with tagged queryKey`, () => { const queryOptionsLike = { @@ -630,8 +631,9 @@ describe(`Query collection type resolution tests`, () => { WrappedResponse, TaggedWrappedKey > - const taggedWrappedQueryKey = - [`query-options-wrapped`] as unknown as TaggedWrappedKey + const taggedWrappedQueryKey = [ + `query-options-wrapped`, + ] as unknown as TaggedWrappedKey const wrappedQueryOptionsLike = { queryKey: taggedWrappedQueryKey, From 4bf448535953ce5bed98bf6e5ac5f2e8ef407968 Mon Sep 17 00:00:00 2001 From: hugiex Date: Mon, 23 Feb 2026 13:39:44 +0700 Subject: [PATCH 3/7] chore(changeset): add query options interop patch note --- .changeset/query-options-interop-types.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/query-options-interop-types.md diff --git a/.changeset/query-options-interop-types.md b/.changeset/query-options-interop-types.md new file mode 100644 index 000000000..10bc6a8e5 --- /dev/null +++ b/.changeset/query-options-interop-types.md @@ -0,0 +1,10 @@ +--- +'@tanstack/query-db-collection': patch +--- + +Improve `queryCollectionOptions` type interoperability with TanStack Query option objects. + +- Accept `queryFn` return types of `T | Promise` instead of Promise-only contracts. +- Align `enabled`, `staleTime`, `refetchInterval`, `retry`, and `retryDelay` with `QueryObserverOptions` typing. +- Support tagged `queryKey` values (`DataTag`) from `queryOptions(...)` spread usage. +- Preserve runtime safety: query collections still require an executable `queryFn`, and wrapped responses still require `select`. From 7a3becc5576ee07a7d7899f40af825eb2f46e29d Mon Sep 17 00:00:00 2001 From: hugiex Date: Mon, 23 Feb 2026 16:11:53 +0700 Subject: [PATCH 4/7] fix(query-db-collection): require queryFn in interop typing --- .changeset/query-options-interop-types.md | 1 + docs/collections/query-collection.md | 5 +-- packages/query-db-collection/src/query.ts | 16 ++++----- .../query-db-collection/tests/query.test-d.ts | 34 ++++++++++++++++++- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/.changeset/query-options-interop-types.md b/.changeset/query-options-interop-types.md index 10bc6a8e5..28dab4644 100644 --- a/.changeset/query-options-interop-types.md +++ b/.changeset/query-options-interop-types.md @@ -7,4 +7,5 @@ Improve `queryCollectionOptions` type interoperability with TanStack Query optio - Accept `queryFn` return types of `T | Promise` instead of Promise-only contracts. - Align `enabled`, `staleTime`, `refetchInterval`, `retry`, and `retryDelay` with `QueryObserverOptions` typing. - Support tagged `queryKey` values (`DataTag`) from `queryOptions(...)` spread usage. +- Keep `queryFn` required in the final `queryCollectionOptions` config (including interop paths) so types match runtime expectations. - Preserve runtime safety: query collections still require an executable `queryFn`, and wrapped responses still require `select`. diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index 4302da516..78cd215fd 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -66,7 +66,7 @@ The `queryCollectionOptions` function accepts the following options: ### Interop with `queryOptions(...)` -If your app already uses a TanStack Query options helper (for example, `queryOptions` from `@tanstack/react-query`), you can pass those options directly into `queryCollectionOptions`: +If your app already uses a TanStack Query options helper (for example, `queryOptions` from `@tanstack/react-query`), you can spread those options into `queryCollectionOptions`. Query collections still require an explicit `queryFn` in the final config type: ```typescript import { QueryClient } from "@tanstack/query-core" @@ -87,13 +87,14 @@ const listOptions = queryOptions({ const todosCollection = createCollection( queryCollectionOptions({ ...listOptions, + queryFn: (context) => listOptions.queryFn!(context), queryClient, getKey: (item) => item.id, }), ) ``` -`queryFn` is still required at runtime for query collections. If it is missing, `queryCollectionOptions` throws `QueryFnRequiredError`. +`queryFn` is required for query collections both in types and at runtime. If it is missing at runtime, `queryCollectionOptions` throws `QueryFnRequiredError`. ### Collection Options diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 0b302d6d7..c6ec0cd7f 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -77,7 +77,7 @@ type QueryOptionsInteropConfig< `queryFn` | `queryKey` > & { queryKey: TaggedQueryKey - queryFn?: ( + queryFn: ( context: QueryFunctionContext< TaggedQueryKey >, @@ -617,15 +617,10 @@ export function queryCollectionOptions< } export function queryCollectionOptions( - config: Omit< - QueryCollectionConfig< - Record, - (context: QueryFunctionContext) => any - >, - `queryFn` - > & { - queryFn?: (context: QueryFunctionContext) => any - }, + config: QueryCollectionConfig< + Record, + (context: QueryFunctionContext) => any + >, ): CollectionConfig< Record, string | number, @@ -662,6 +657,7 @@ export function queryCollectionOptions( throw new QueryKeyRequiredError() } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!queryFn) { throw new QueryFnRequiredError() } diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index 2d384bbac..f274bbb9f 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -10,7 +10,11 @@ import { import { QueryClient } from '@tanstack/query-core' import { z } from 'zod' import { queryCollectionOptions } from '../src/query' -import type { DataTag, QueryObserverOptions } from '@tanstack/query-core' +import type { + DataTag, + QueryFunctionContext, + QueryObserverOptions, +} from '@tanstack/query-core' import type { QueryCollectionConfig, QueryCollectionUtils } from '../src/query' import type { DeleteMutationFnParams, @@ -618,6 +622,34 @@ describe(`Query collection type resolution tests`, () => { expectTypeOf(options.getKey).parameters.toEqualTypeOf<[NumberItem]>() }) + it(`should require explicit queryFn when source type marks queryFn optional`, () => { + const queryOptionsLike: { + queryKey: TaggedNumbersKey + queryFn?: ( + context: QueryFunctionContext, + ) => Array | Promise> + } = { + queryKey: taggedNumbersQueryKey, + queryFn: () => Promise.resolve([{ id: 1, value: `one` }]), + } + + // @ts-expect-error - interop configs require queryFn even when source type marks it optional + queryCollectionOptions({ + ...queryOptionsLike, + queryClient, + getKey: (item) => item.id, + }) + + const options = queryCollectionOptions({ + ...queryOptionsLike, + queryFn: (context) => queryOptionsLike.queryFn!(context), + queryClient, + getKey: (item) => item.id, + }) + + expectTypeOf(options.getKey).parameters.toEqualTypeOf<[NumberItem]>() + }) + it(`should require select for wrapped queryOptions-like responses`, () => { type WrappedResponse = { total: number From dbd6ba6a6168cac5fbee844020caa8da5502d5de Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Mon, 23 Feb 2026 11:09:48 +0100 Subject: [PATCH 5/7] refactor(query-db-collection): remove interop overloads, keep type widenings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The broader type widenings (queryFn return T | Promise, enabled as QueryObserverOptions['enabled'], TQueryData generics) are sufficient to handle queryOptions(...) interop without dedicated interop overloads. This removes TaggedQueryKey, QueryOptionsInteropConfig, and the two interop-specific overloads, reducing type complexity while preserving all 24 type tests passing. Existing tests are unchanged from main — only new interop tests are added. Co-Authored-By: Claude Opus 4.6 --- packages/query-db-collection/src/query.ts | 92 ------------------- .../query-db-collection/tests/query.test-d.ts | 51 +++++----- 2 files changed, 27 insertions(+), 116 deletions(-) diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index c6ec0cd7f..c5feecbdf 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -20,7 +20,6 @@ import type { UtilsRecord, } from '@tanstack/db' import type { - DataTag, FetchStatus, QueryClient, QueryFunctionContext, @@ -48,41 +47,6 @@ type InferSchemaInput = T extends StandardSchemaV1 : Record type TQueryKeyBuilder = (opts: LoadSubsetOptions) => TQueryKey -type TaggedQueryKey = DataTag< - TQueryKey, - TQueryData, - TError -> -type QueryOptionsInteropConfig< - T extends object, - TError, - TQueryKey extends QueryKey, - TQueryData, - TKey extends string | number = string | number, - TSchema extends StandardSchemaV1 = never, -> = Omit< - QueryCollectionConfig< - T, - ( - context: QueryFunctionContext< - TaggedQueryKey - >, - ) => Promise, - TError, - TaggedQueryKey, - TKey, - TSchema, - TQueryData - >, - `queryFn` | `queryKey` -> & { - queryKey: TaggedQueryKey - queryFn: ( - context: QueryFunctionContext< - TaggedQueryKey - >, - ) => TQueryData | Promise -} /** * Configuration options for creating a Query Collection @@ -500,35 +464,6 @@ export function queryCollectionOptions< utils: QueryCollectionUtils } -// Interop overload for queryOptions(...) + select (no schema) -export function queryCollectionOptions< - T extends object, - TError = unknown, - TQueryKey extends QueryKey = QueryKey, - TKey extends string | number = string | number, - TQueryData = unknown, ->( - config: QueryOptionsInteropConfig< - T, - TError, - TQueryKey, - TQueryData, - TKey, - never - > & { - schema?: never // prohibit schema - select: (data: TQueryData) => Array - }, -): CollectionConfig< - T, - TKey, - never, - QueryCollectionUtils -> & { - schema?: never // no schema in the result - utils: QueryCollectionUtils -} - // Overload for when schema is provided export function queryCollectionOptions< T extends StandardSchemaV1, @@ -589,33 +524,6 @@ export function queryCollectionOptions< utils: QueryCollectionUtils } -// Interop overload for queryOptions(...) (no schema, no select) -export function queryCollectionOptions< - T extends object, - TError = unknown, - TQueryKey extends QueryKey = QueryKey, - TKey extends string | number = string | number, ->( - config: QueryOptionsInteropConfig< - T, - TError, - TQueryKey, - Array, - TKey, - never - > & { - schema?: never // prohibit schema - }, -): CollectionConfig< - T, - TKey, - never, - QueryCollectionUtils -> & { - schema?: never // no schema in the result - utils: QueryCollectionUtils -} - export function queryCollectionOptions( config: QueryCollectionConfig< Record, diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index f274bbb9f..5722445b0 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -228,7 +228,7 @@ describe(`Query collection type resolution tests`, () => { queryFn: async (): Promise> => { return [] as Array }, - getKey: (item: TodoType) => item.id, + getKey: (item) => item.id, }) // Should infer TodoType from queryFn @@ -241,17 +241,15 @@ describe(`Query collection type resolution tests`, () => { name: string } - const invalidConfig = { + queryCollectionOptions({ queryClient, queryKey: [`explicit-priority`], + // @ts-expect-error – queryFn doesn't match the explicit type queryFn: async (): Promise> => { return [] as Array }, - getKey: (item: UserType) => item.id, - } - - // @ts-expect-error – queryFn doesn't match the explicit type - queryCollectionOptions(invalidConfig) + getKey: (item) => item.id, + }) }) it(`should prioritize schema over queryFn`, () => { @@ -288,18 +286,20 @@ describe(`Query collection type resolution tests`, () => { email: z.string(), }) - const invalidConfig = { + const options = queryCollectionOptions({ queryClient, queryKey: [`schema-priority`], queryFn: async () => { return [] as Array }, + // @ts-expect-error – queryFn doesn't match the schema type schema: userSchema, - getKey: (item: z.infer) => item.id, - } + getKey: (item) => item.id, + }) - // @ts-expect-error – queryFn doesn't match the schema type - queryCollectionOptions(invalidConfig) + // Should use schema type, not TodoType from queryFn + type ExpectedType = z.infer + expectTypeOf(options.getKey).parameters.toEqualTypeOf<[ExpectedType]>() }) it(`should maintain backward compatibility with explicit types`, () => { @@ -351,31 +351,34 @@ describe(`Query collection type resolution tests`, () => { }) it(`should error when queryFn returns wrapped data without select`, () => { - type UserDataType = { - id: string - name: string - email: string - } + const userData = z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + }) + + type UserDataType = z.infer type WrappedResponse = { metadata: string data: Array } - const invalidConfig = { + queryCollectionOptions({ queryClient, queryKey: [`wrapped-no-select`], + // @ts-expect-error - queryFn returns wrapped data but no select provided queryFn: (): Promise => { return Promise.resolve({ metadata: `example`, data: [], }) }, - getKey: () => `1`, - } - - // @ts-expect-error - queryFn returns wrapped data but no select provided - queryCollectionOptions(invalidConfig) + // @ts-expect-error - schema type conflicts with queryFn return type + schema: userData, + // @ts-expect-error - item type is inferred as object due to type mismatch + getKey: (item) => item.id, + }) }) it(`select properly extracts array from wrapped response`, () => { @@ -697,9 +700,9 @@ describe(`Query collection type resolution tests`, () => { }) it(`should still require queryFn for plain configs`, () => { + // @ts-expect-error - queryFn is required for plain configs queryCollectionOptions({ queryClient, - // @ts-expect-error - queryFn is required for plain (non-queryOptions-like) configs queryKey: [`query-options-missing-query-fn`], getKey: (item) => item.id, }) From 3b08a6fd3f0961bea899311766187579162106da Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Mon, 23 Feb 2026 12:25:34 +0100 Subject: [PATCH 6/7] Updated changeset --- .changeset/query-options-interop-types.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.changeset/query-options-interop-types.md b/.changeset/query-options-interop-types.md index 28dab4644..60efbcf2a 100644 --- a/.changeset/query-options-interop-types.md +++ b/.changeset/query-options-interop-types.md @@ -2,10 +2,9 @@ '@tanstack/query-db-collection': patch --- -Improve `queryCollectionOptions` type interoperability with TanStack Query option objects. +Improve `queryCollectionOptions` type compatibility with TanStack Query option objects. -- Accept `queryFn` return types of `T | Promise` instead of Promise-only contracts. +- Accept `queryFn` return types of `T | Promise` instead of requiring `Promise`. - Align `enabled`, `staleTime`, `refetchInterval`, `retry`, and `retryDelay` with `QueryObserverOptions` typing. - Support tagged `queryKey` values (`DataTag`) from `queryOptions(...)` spread usage. -- Keep `queryFn` required in the final `queryCollectionOptions` config (including interop paths) so types match runtime expectations. - Preserve runtime safety: query collections still require an executable `queryFn`, and wrapped responses still require `select`. From a54caa0ed1233004362a71371b0a8ea61b60e294 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Mon, 23 Feb 2026 12:25:41 +0100 Subject: [PATCH 7/7] Updated docs --- docs/collections/query-collection.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index 78cd215fd..38fecd4b4 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -64,9 +64,9 @@ The `queryCollectionOptions` function accepts the following options: - `staleTime`: How long data is considered fresh - `meta`: Optional metadata that will be passed to the query function context -### Interop with `queryOptions(...)` +### Using with `queryOptions(...)` -If your app already uses a TanStack Query options helper (for example, `queryOptions` from `@tanstack/react-query`), you can spread those options into `queryCollectionOptions`. Query collections still require an explicit `queryFn` in the final config type: +If your app already uses TanStack Query's `queryOptions` helper (e.g. from `@tanstack/react-query`), you can spread those options into `queryCollectionOptions`. Note that `queryFn` must be explicitly provided since query collections require it both in types and at runtime: ```typescript import { QueryClient } from "@tanstack/query-core" @@ -94,7 +94,7 @@ const todosCollection = createCollection( ) ``` -`queryFn` is required for query collections both in types and at runtime. If it is missing at runtime, `queryCollectionOptions` throws `QueryFnRequiredError`. +If `queryFn` is missing at runtime, `queryCollectionOptions` throws `QueryFnRequiredError`. ### Collection Options