@@ -24,69 +24,114 @@ import type {
24
24
UpdateMutationFnParams ,
25
25
UtilsRecord ,
26
26
} from "@tanstack/db"
27
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
27
28
28
29
// Re-export for external use
29
30
export type { SyncOperation } from "./manual-sync"
30
31
32
+ // Schema output type inference helper (matches electric.ts pattern)
33
+ type InferSchemaOutput < T > = T extends StandardSchemaV1
34
+ ? StandardSchemaV1 . InferOutput < T > extends object
35
+ ? StandardSchemaV1 . InferOutput < T >
36
+ : Record < string , unknown >
37
+ : Record < string , unknown >
38
+
39
+ // QueryFn return type inference helper
40
+ type InferQueryFnOutput < TQueryFn > = TQueryFn extends (
41
+ context : QueryFunctionContext < any >
42
+ ) => Promise < Array < infer TItem > >
43
+ ? TItem extends object
44
+ ? TItem
45
+ : Record < string , unknown >
46
+ : Record < string , unknown >
47
+
48
+ // Type resolution system with priority order (matches electric.ts pattern)
49
+ type ResolveType <
50
+ TExplicit extends object | unknown = unknown ,
51
+ TSchema extends StandardSchemaV1 = never ,
52
+ TQueryFn = unknown ,
53
+ > = unknown extends TExplicit
54
+ ? [ TSchema ] extends [ never ]
55
+ ? InferQueryFnOutput < TQueryFn >
56
+ : InferSchemaOutput < TSchema >
57
+ : TExplicit
58
+
31
59
/**
32
60
* Configuration options for creating a Query Collection
33
- * @template TItem - The type of items stored in the collection
61
+ * @template TExplicit - The explicit type of items stored in the collection (highest priority)
62
+ * @template TSchema - The schema type for validation and type inference (second priority)
63
+ * @template TQueryFn - The queryFn type for inferring return type (third priority)
34
64
* @template TError - The type of errors that can occur during queries
35
65
* @template TQueryKey - The type of the query key
36
66
*/
37
67
export interface QueryCollectionConfig <
38
- TItem extends object ,
68
+ TExplicit extends object = object ,
69
+ TSchema extends StandardSchemaV1 = never ,
70
+ TQueryFn extends (
71
+ context : QueryFunctionContext < any >
72
+ ) => Promise < Array < any > > = (
73
+ context : QueryFunctionContext < any >
74
+ ) => Promise < Array < any > > ,
39
75
TError = unknown ,
40
76
TQueryKey extends QueryKey = QueryKey ,
41
77
> {
42
78
/** The query key used by TanStack Query to identify this query */
43
79
queryKey : TQueryKey
44
80
/** Function that fetches data from the server. Must return the complete collection state */
45
- queryFn : ( context : QueryFunctionContext < TQueryKey > ) => Promise < Array < TItem > >
81
+ queryFn : TQueryFn extends (
82
+ context : QueryFunctionContext < TQueryKey >
83
+ ) => Promise < Array < any > >
84
+ ? TQueryFn
85
+ : (
86
+ context : QueryFunctionContext < TQueryKey >
87
+ ) => Promise < Array < ResolveType < TExplicit , TSchema , TQueryFn > > >
88
+
46
89
/** The TanStack Query client instance */
47
90
queryClient : QueryClient
48
91
49
92
// Query-specific options
50
93
/** Whether the query should automatically run (default: true) */
51
94
enabled ?: boolean
52
95
refetchInterval ?: QueryObserverOptions <
53
- Array < TItem > ,
96
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
54
97
TError ,
55
- Array < TItem > ,
56
- Array < TItem > ,
98
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
99
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
57
100
TQueryKey
58
101
> [ `refetchInterval`]
59
102
retry ?: QueryObserverOptions <
60
- Array < TItem > ,
103
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
61
104
TError ,
62
- Array < TItem > ,
63
- Array < TItem > ,
105
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
106
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
64
107
TQueryKey
65
108
> [ `retry`]
66
109
retryDelay ?: QueryObserverOptions <
67
- Array < TItem > ,
110
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
68
111
TError ,
69
- Array < TItem > ,
70
- Array < TItem > ,
112
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
113
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
71
114
TQueryKey
72
115
> [ `retryDelay`]
73
116
staleTime ?: QueryObserverOptions <
74
- Array < TItem > ,
117
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
75
118
TError ,
76
- Array < TItem > ,
77
- Array < TItem > ,
119
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
120
+ Array < ResolveType < TExplicit , TSchema , TQueryFn > > ,
78
121
TQueryKey
79
122
> [ `staleTime`]
80
123
81
124
// Standard Collection configuration properties
82
125
/** Unique identifier for the collection */
83
126
id ?: string
84
127
/** Function to extract the unique key from an item */
85
- getKey : CollectionConfig < TItem > [ `getKey`]
128
+ getKey : CollectionConfig < ResolveType < TExplicit , TSchema , TQueryFn > > [ `getKey`]
86
129
/** Schema for validating items */
87
- schema ?: CollectionConfig < TItem > [ `schema`]
88
- sync ?: CollectionConfig < TItem > [ `sync`]
89
- startSync ?: CollectionConfig < TItem > [ `startSync`]
130
+ schema ?: TSchema
131
+ sync ?: CollectionConfig < ResolveType < TExplicit , TSchema , TQueryFn > > [ `sync`]
132
+ startSync ?: CollectionConfig <
133
+ ResolveType < TExplicit , TSchema , TQueryFn >
134
+ > [ `startSync`]
90
135
91
136
// Direct persistence handlers
92
137
/**
@@ -129,7 +174,7 @@ export interface QueryCollectionConfig<
129
174
* }
130
175
* }
131
176
*/
132
- onInsert ?: InsertMutationFn < TItem >
177
+ onInsert ?: InsertMutationFn < ResolveType < TExplicit , TSchema , TQueryFn > >
133
178
134
179
/**
135
180
* Optional asynchronous handler function called before an update operation
@@ -182,7 +227,7 @@ export interface QueryCollectionConfig<
182
227
* return { refetch: false } // Skip automatic refetch since we handled it manually
183
228
* }
184
229
*/
185
- onUpdate ?: UpdateMutationFn < TItem >
230
+ onUpdate ?: UpdateMutationFn < ResolveType < TExplicit , TSchema , TQueryFn > >
186
231
187
232
/**
188
233
* Optional asynchronous handler function called before a delete operation
@@ -228,8 +273,7 @@ export interface QueryCollectionConfig<
228
273
* return { refetch: false } // Skip automatic refetch since we handled it manually
229
274
* }
230
275
*/
231
- onDelete ?: DeleteMutationFn < TItem >
232
- // TODO type returning { refetch: boolean }
276
+ onDelete ?: DeleteMutationFn < ResolveType < TExplicit , TSchema , TQueryFn > >
233
277
234
278
/**
235
279
* Metadata to pass to the query.
@@ -289,16 +333,55 @@ export interface QueryCollectionUtils<
289
333
* Creates query collection options for use with a standard Collection.
290
334
* This integrates TanStack Query with TanStack DB for automatic synchronization.
291
335
*
336
+ * Supports automatic type inference following the priority order:
337
+ * 1. Explicit type (highest priority)
338
+ * 2. Schema inference (second priority)
339
+ * 3. QueryFn return type inference (third priority)
340
+ * 4. Fallback to Record<string, unknown>
341
+ *
342
+ * @template TExplicit - The explicit type of items in the collection (highest priority)
343
+ * @template TSchema - The schema type for validation and type inference (second priority)
344
+ * @template TQueryFn - The queryFn type for inferring return type (third priority)
345
+ * @template TError - The type of errors that can occur during queries
346
+ * @template TQueryKey - The type of the query key
347
+ * @template TKey - The type of the item keys
348
+ * @template TInsertInput - The type accepted for insert operations
292
349
* @param config - Configuration options for the Query collection
293
350
* @returns Collection options with utilities for direct writes and manual operations
294
351
*
295
352
* @example
296
- * // Basic usage
353
+ * // Type inferred from queryFn return type (NEW!)
354
+ * const todosCollection = createCollection(
355
+ * queryCollectionOptions({
356
+ * queryKey: ['todos'],
357
+ * queryFn: async () => {
358
+ * const response = await fetch('/api/todos')
359
+ * return response.json() as Todo[] // Type automatically inferred!
360
+ * },
361
+ * queryClient,
362
+ * getKey: (item) => item.id, // item is typed as Todo
363
+ * })
364
+ * )
365
+ *
366
+ * @example
367
+ * // Explicit type (highest priority)
368
+ * const todosCollection = createCollection<Todo>(
369
+ * queryCollectionOptions({
370
+ * queryKey: ['todos'],
371
+ * queryFn: async () => fetch('/api/todos').then(r => r.json()),
372
+ * queryClient,
373
+ * getKey: (item) => item.id,
374
+ * })
375
+ * )
376
+ *
377
+ * @example
378
+ * // Schema inference (second priority)
297
379
* const todosCollection = createCollection(
298
380
* queryCollectionOptions({
299
381
* queryKey: ['todos'],
300
382
* queryFn: async () => fetch('/api/todos').then(r => r.json()),
301
383
* queryClient,
384
+ * schema: todoSchema, // Type inferred from schema
302
385
* getKey: (item) => item.id,
303
386
* })
304
387
* )
@@ -324,16 +407,28 @@ export interface QueryCollectionUtils<
324
407
* )
325
408
*/
326
409
export function queryCollectionOptions <
327
- TItem extends object ,
410
+ TExplicit extends object = object ,
411
+ TSchema extends StandardSchemaV1 = never ,
412
+ TQueryFn extends (
413
+ context : QueryFunctionContext < any >
414
+ ) => Promise < Array < any > > = (
415
+ context : QueryFunctionContext < any >
416
+ ) => Promise < Array < any > > ,
328
417
TError = unknown ,
329
418
TQueryKey extends QueryKey = QueryKey ,
330
419
TKey extends string | number = string | number ,
331
- TInsertInput extends object = TItem ,
420
+ TInsertInput extends object = ResolveType < TExplicit , TSchema , TQueryFn > ,
332
421
> (
333
- config : QueryCollectionConfig < TItem , TError , TQueryKey >
334
- ) : CollectionConfig < TItem > & {
335
- utils : QueryCollectionUtils < TItem , TKey , TInsertInput >
422
+ config : QueryCollectionConfig < TExplicit , TSchema , TQueryFn , TError , TQueryKey >
423
+ ) : CollectionConfig < ResolveType < TExplicit , TSchema , TQueryFn > > & {
424
+ utils : QueryCollectionUtils <
425
+ ResolveType < TExplicit , TSchema , TQueryFn > ,
426
+ TKey ,
427
+ TInsertInput
428
+ >
336
429
} {
430
+ type TItem = ResolveType < TExplicit , TSchema , TQueryFn >
431
+
337
432
const {
338
433
queryKey,
339
434
queryFn,
0 commit comments