Skip to content

Commit 0be0c9a

Browse files
committed
fix(query-db-collection): resolve TypeScript type error for QueryCollectionConfig
Fixed TypeScript type resolution issue where QueryCollectionConfig failed to recognize inherited properties (getKey, onInsert, onUpdate, etc.) when using queryCollectionOptions without a schema. The issue was caused by QueryCollectionConfig extending BaseCollectionConfig while also having a conditional type for the queryFn property. TypeScript couldn't properly resolve the inherited properties in this scenario. Solution: Changed QueryCollectionConfig to use Omit<BaseCollectionConfig, ...> pattern, consistent with ElectricCollectionConfig and PowerSyncCollectionConfig. Changes: - Refactored QueryCollectionConfig to use Omit pattern for consistency - Explicitly declares mutation handlers with custom return type { refetch?: boolean } - Removed unused imports (StringCollationConfig, SyncMode) - Added test cases to verify no-schema usage works correctly
1 parent a90f95e commit 0be0c9a

File tree

3 files changed

+132
-8
lines changed

3 files changed

+132
-8
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
"@tanstack/query-db-collection": patch
3+
---
4+
5+
Fix TypeScript type resolution for QueryCollectionConfig when using queryCollectionOptions without a schema.
6+
7+
Previously, the `QueryCollectionConfig` interface extended `BaseCollectionConfig`, but TypeScript failed to resolve inherited properties like `getKey`, `onInsert`, `onUpdate`, etc. when the interface contained a conditional type for the `queryFn` property. This caused type errors when trying to use `queryCollectionOptions` without a schema.
8+
9+
**Before:**
10+
11+
```typescript
12+
// This would fail with TypeScript error:
13+
// "Property 'getKey' does not exist on type 'QueryCollectionConfig<...>'"
14+
const options = queryCollectionOptions({
15+
queryKey: ["todos"],
16+
queryFn: async (): Promise<Array<Todo>> => {
17+
const response = await fetch("/api/todos")
18+
return response.json()
19+
},
20+
queryClient,
21+
getKey: (item) => item.id, // ❌ Type error
22+
})
23+
```
24+
25+
**After:**
26+
27+
```typescript
28+
// Now works correctly!
29+
const options = queryCollectionOptions({
30+
queryKey: ["todos"],
31+
queryFn: async (): Promise<Array<Todo>> => {
32+
const response = await fetch("/api/todos")
33+
return response.json()
34+
},
35+
queryClient,
36+
getKey: (item) => item.id, // ✅ Works
37+
})
38+
39+
const collection = createCollection(options) // ✅ Fully typed
40+
```
41+
42+
**Changes:**
43+
44+
- Changed `QueryCollectionConfig` to use `Omit<BaseCollectionConfig<...>, 'onInsert' | 'onUpdate' | 'onDelete'>` pattern
45+
- This matches the approach used by `ElectricCollectionConfig` and `PowerSyncCollectionConfig` for consistency
46+
- Explicitly declares mutation handlers with custom return type `{ refetch?: boolean }`
47+
- This resolves the TypeScript type resolution issue with conditional types
48+
- All functionality remains the same - this is purely a type-level fix
49+
- Added test cases to verify the no-schema use case works correctly

packages/query-db-collection/src/query.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ import type {
1111
BaseCollectionConfig,
1212
ChangeMessage,
1313
CollectionConfig,
14+
DeleteMutationFn,
1415
DeleteMutationFnParams,
16+
InsertMutationFn,
1517
InsertMutationFnParams,
1618
LoadSubsetOptions,
1719
SyncConfig,
20+
UpdateMutationFn,
1821
UpdateMutationFnParams,
1922
UtilsRecord,
2023
} from "@tanstack/db"
@@ -66,7 +69,10 @@ export interface QueryCollectionConfig<
6669
TKey extends string | number = string | number,
6770
TSchema extends StandardSchemaV1 = never,
6871
TQueryData = Awaited<ReturnType<TQueryFn>>,
69-
> extends BaseCollectionConfig<T, TKey, TSchema> {
72+
> extends Omit<
73+
BaseCollectionConfig<T, TKey, TSchema, UtilsRecord, any>,
74+
`onInsert` | `onUpdate` | `onDelete`
75+
> {
7076
/** The query key used by TanStack Query to identify this query */
7177
queryKey: TQueryKey | TQueryKeyBuilder<TQueryKey>
7278
/** Function that fetches data from the server. Must return the complete collection state */
@@ -133,6 +139,30 @@ export interface QueryCollectionConfig<
133139
* }
134140
*/
135141
meta?: Record<string, unknown>
142+
143+
/**
144+
* Optional asynchronous handler called when items are inserted into the collection
145+
* Allows persisting changes to a backend and optionally controlling refetch behavior
146+
* @param params Object containing transaction and collection information
147+
* @returns Promise that can return { refetch?: boolean } to control whether to refetch after insert
148+
*/
149+
onInsert?: InsertMutationFn<T, TKey, UtilsRecord, { refetch?: boolean }>
150+
151+
/**
152+
* Optional asynchronous handler called when items are updated in the collection
153+
* Allows persisting changes to a backend and optionally controlling refetch behavior
154+
* @param params Object containing transaction and collection information
155+
* @returns Promise that can return { refetch?: boolean } to control whether to refetch after update
156+
*/
157+
onUpdate?: UpdateMutationFn<T, TKey, UtilsRecord, { refetch?: boolean }>
158+
159+
/**
160+
* Optional asynchronous handler called when items are deleted from the collection
161+
* Allows persisting changes to a backend and optionally controlling refetch behavior
162+
* @param params Object containing transaction and collection information
163+
* @returns Promise that can return { refetch?: boolean } to control whether to refetch after delete
164+
*/
165+
onDelete?: DeleteMutationFn<T, TKey, UtilsRecord, { refetch?: boolean }>
136166
}
137167

138168
/**
@@ -518,13 +548,8 @@ export function queryCollectionOptions<
518548
}
519549

520550
export function queryCollectionOptions(
521-
config: QueryCollectionConfig<Record<string, unknown>>
522-
): CollectionConfig<
523-
Record<string, unknown>,
524-
string | number,
525-
never,
526-
QueryCollectionUtils
527-
> & {
551+
config: QueryCollectionConfig<any, any, any, any, any, any, any>
552+
): CollectionConfig<any, any, any, any> & {
528553
utils: QueryCollectionUtils
529554
} {
530555
const {

packages/query-db-collection/tests/query.test-d.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,56 @@ describe(`Query collection type resolution tests`, () => {
302302
})
303303
})
304304

305+
describe(`no schema with type inference from queryFn`, () => {
306+
it(`should work without schema when queryFn has explicit return type`, () => {
307+
interface Todo {
308+
id: string
309+
title: string
310+
completed: boolean
311+
}
312+
313+
const options = queryCollectionOptions({
314+
queryKey: [`todos-no-schema`],
315+
queryFn: async (): Promise<Array<Todo>> => {
316+
return [] as Array<Todo>
317+
},
318+
queryClient,
319+
getKey: (item) => item.id,
320+
})
321+
322+
const collection = createCollection(options)
323+
324+
// Verify types are correctly inferred
325+
expectTypeOf(options.getKey).parameters.toEqualTypeOf<[Todo]>()
326+
expectTypeOf(collection.toArray).toEqualTypeOf<Array<Todo>>()
327+
})
328+
329+
it(`should work without schema when queryFn return type is inferred`, () => {
330+
interface Todo {
331+
id: string
332+
title: string
333+
completed: boolean
334+
}
335+
336+
const fetchTodos = async (): Promise<Array<Todo>> => {
337+
return [] as Array<Todo>
338+
}
339+
340+
const options = queryCollectionOptions({
341+
queryKey: [`todos-no-schema-inferred`],
342+
queryFn: fetchTodos,
343+
queryClient,
344+
getKey: (item) => item.id,
345+
})
346+
347+
const collection = createCollection(options)
348+
349+
// Verify types are correctly inferred
350+
expectTypeOf(options.getKey).parameters.toEqualTypeOf<[Todo]>()
351+
expectTypeOf(collection.toArray).toEqualTypeOf<Array<Todo>>()
352+
})
353+
})
354+
305355
describe(`select type inference`, () => {
306356
it(`queryFn type inference`, () => {
307357
const dataSchema = z.object({

0 commit comments

Comments
 (0)