Skip to content

Commit f66f2cf

Browse files
authored
Modify type of query function's meta property to be more precise (#857)
* Type tests reproducing the problem with the current type of the meta property * Augment tanstack query-core module to provide precise type for meta property * Fix linting * Changeset * Fix failing unit test after rebase
1 parent f73a1f8 commit f66f2cf

File tree

6 files changed

+109
-20
lines changed

6 files changed

+109
-20
lines changed

.changeset/eighty-ideas-clean.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/query-db-collection": patch
3+
---
4+
5+
Improved the type of the queryFn's ctx.meta property of the Query Collection to include the loadSubsetOptions

packages/query-db-collection/e2e/query.e2e.test.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
generateSeedData,
2020
} from "../../db-collection-e2e/src/index"
2121
import { applyPredicates, buildQueryKey } from "./query-filter"
22-
import type { LoadSubsetOptions } from "@tanstack/db"
2322
import type {
2423
Comment as E2EComment,
2524
Post as E2EPost,
@@ -94,9 +93,7 @@ describe(`Query Collection E2E Tests`, () => {
9493
queryKey: (opts) => buildQueryKey(`users`, opts),
9594
syncMode: `on-demand`,
9695
queryFn: (ctx) => {
97-
const options = ctx.meta?.loadSubsetOptions as
98-
| LoadSubsetOptions
99-
| undefined
96+
const options = ctx.meta?.loadSubsetOptions
10097
const filtered = applyPredicates(seedData.users, options)
10198
return Promise.resolve(filtered)
10299
},
@@ -112,9 +109,7 @@ describe(`Query Collection E2E Tests`, () => {
112109
queryKey: (opts) => buildQueryKey(`posts`, opts),
113110
syncMode: `on-demand`,
114111
queryFn: (ctx) => {
115-
const options = ctx.meta?.loadSubsetOptions as
116-
| LoadSubsetOptions
117-
| undefined
112+
const options = ctx.meta?.loadSubsetOptions
118113
const filtered = applyPredicates(seedData.posts, options)
119114
return Promise.resolve(filtered)
120115
},
@@ -130,9 +125,7 @@ describe(`Query Collection E2E Tests`, () => {
130125
queryKey: (opts) => buildQueryKey(`comments`, opts),
131126
syncMode: `on-demand`,
132127
queryFn: (ctx) => {
133-
const options = ctx.meta?.loadSubsetOptions as
134-
| LoadSubsetOptions
135-
| undefined
128+
const options = ctx.meta?.loadSubsetOptions
136129
const filtered = applyPredicates(seedData.comments, options)
137130
return Promise.resolve(filtered)
138131
},

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export {
22
queryCollectionOptions,
33
type QueryCollectionConfig,
4+
type QueryCollectionMeta,
45
type QueryCollectionUtils,
56
type SyncOperation,
67
} from "./query"

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"
3131
// Re-export for external use
3232
export type { SyncOperation } from "./manual-sync"
3333

34+
/**
35+
* Base type for Query Collection meta properties.
36+
* Users can extend this type when augmenting the @tanstack/query-core module
37+
* to add their own custom properties while preserving loadSubsetOptions.
38+
*
39+
* @example
40+
* ```typescript
41+
* declare module "@tanstack/query-core" {
42+
* interface Register {
43+
* queryMeta: QueryCollectionMeta & {
44+
* myCustomProperty: string
45+
* }
46+
* }
47+
* }
48+
* ```
49+
*/
50+
export type QueryCollectionMeta = Record<string, unknown> & {
51+
loadSubsetOptions: LoadSubsetOptions
52+
}
53+
54+
// Module augmentation to extend TanStack Query's Register interface
55+
// This ensures that ctx.meta always includes loadSubsetOptions
56+
// We extend Record<string, unknown> to preserve the ability to add other meta properties
57+
declare module "@tanstack/query-core" {
58+
interface Register {
59+
queryMeta: QueryCollectionMeta
60+
}
61+
}
62+
3463
// Schema output type inference helper (matches electric.ts pattern)
3564
type InferSchemaOutput<T> = T extends StandardSchemaV1
3665
? StandardSchemaV1.InferOutput<T> extends object

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import {
55
createLiveQueryCollection,
66
eq,
77
gt,
8+
parseLoadSubsetOptions,
89
} from "@tanstack/db"
910
import { QueryClient } from "@tanstack/query-core"
1011
import { z } from "zod"
1112
import { queryCollectionOptions } from "../src/query"
13+
import type { QueryCollectionConfig } from "../src/query"
1214
import type {
1315
DeleteMutationFnParams,
1416
InsertMutationFnParams,
17+
LoadSubsetOptions,
1518
UpdateMutationFnParams,
1619
} from "@tanstack/db"
1720

@@ -403,4 +406,69 @@ describe(`Query collection type resolution tests`, () => {
403406
expectTypeOf(selectUserData).parameters.toEqualTypeOf<[ResponseType]>()
404407
})
405408
})
409+
410+
describe(`loadSubsetOptions type inference`, () => {
411+
interface TestItem {
412+
id: string
413+
name: string
414+
}
415+
416+
it(`should type loadSubsetOptions as LoadSubsetOptions in queryFn`, () => {
417+
const config: QueryCollectionConfig<TestItem> = {
418+
id: `loadSubsetTest`,
419+
queryClient,
420+
queryKey: [`loadSubsetTest`],
421+
queryFn: (ctx) => {
422+
// Verify that loadSubsetOptions is assignable to LoadSubsetOptions
423+
// This ensures it can be used where LoadSubsetOptions is expected
424+
expectTypeOf(
425+
ctx.meta!.loadSubsetOptions
426+
).toExtend<LoadSubsetOptions>()
427+
// so that parseLoadSubsetOptions can be called without type errors
428+
parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)
429+
// The fact that this call compiles without errors verifies that
430+
// ctx.meta.loadSubsetOptions is typed correctly as LoadSubsetOptions
431+
return Promise.resolve([])
432+
},
433+
getKey: (item) => item.id,
434+
syncMode: `on-demand`,
435+
}
436+
437+
const options = queryCollectionOptions(config)
438+
createCollection(options)
439+
})
440+
441+
it(`should allow meta to contain additional properties beyond loadSubsetOptions`, () => {
442+
const config: QueryCollectionConfig<TestItem> = {
443+
id: `loadSubsetTest`,
444+
queryClient,
445+
queryKey: [`loadSubsetTest`],
446+
queryFn: (ctx) => {
447+
// Verify that an object with loadSubsetOptions plus other properties
448+
// can be assigned to ctx.meta's type. This ensures the type is not too restrictive.
449+
const metaWithExtra = {
450+
loadSubsetOptions: ctx.meta!.loadSubsetOptions,
451+
customProperty: `test`,
452+
anotherProperty: 123,
453+
}
454+
455+
// Test that this object can be assigned to ctx.meta's type
456+
// This verifies that ctx.meta allows additional properties beyond loadSubsetOptions
457+
const typedMeta: typeof ctx.meta = metaWithExtra
458+
459+
// Verify the assignment worked (this will fail at compile time if types don't match)
460+
expectTypeOf(
461+
typedMeta.loadSubsetOptions
462+
).toExtend<LoadSubsetOptions>()
463+
464+
return Promise.resolve([])
465+
},
466+
getKey: (item) => item.id,
467+
syncMode: `on-demand`,
468+
}
469+
470+
const options = queryCollectionOptions(config)
471+
createCollection(options)
472+
})
473+
})
406474
})

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type {
1313
Collection,
1414
DeleteMutationFnParams,
1515
InsertMutationFnParams,
16-
LoadSubsetOptions,
1716
TransactionWithMutations,
1817
UpdateMutationFnParams,
1918
} from "@tanstack/db"
@@ -455,9 +454,7 @@ describe(`QueryCollection`, () => {
455454
const queryFn = vi
456455
.fn()
457456
.mockImplementation((ctx: QueryFunctionContext<any>) => {
458-
const loadSubsetOptions = ctx.meta?.loadSubsetOptions as
459-
| LoadSubsetOptions
460-
| undefined
457+
const loadSubsetOptions = ctx.meta?.loadSubsetOptions
461458
// Verify where clause is present
462459
expect(loadSubsetOptions?.where).toBeDefined()
463460
expect(loadSubsetOptions?.where).not.toBeNull()
@@ -515,9 +512,7 @@ describe(`QueryCollection`, () => {
515512
const queryFn = vi
516513
.fn()
517514
.mockImplementation((ctx: QueryFunctionContext<any>) => {
518-
const loadSubsetOptions = ctx.meta?.loadSubsetOptions as
519-
| LoadSubsetOptions
520-
| undefined
515+
const loadSubsetOptions = ctx.meta?.loadSubsetOptions
521516
// Verify where clause is present (this was the bug - it was undefined/null before the fix)
522517
expect(loadSubsetOptions?.where).toBeDefined()
523518
expect(loadSubsetOptions?.where).not.toBeNull()
@@ -3678,9 +3673,7 @@ describe(`QueryCollection`, () => {
36783673
]
36793674

36803675
const queryFn = vi.fn((ctx: QueryFunctionContext) => {
3681-
const loadSubsetOptions = ctx.meta?.loadSubsetOptions as
3682-
| LoadSubsetOptions
3683-
| undefined
3676+
const loadSubsetOptions = ctx.meta?.loadSubsetOptions
36843677
// Filter items based on the where clause if present
36853678
if (loadSubsetOptions?.where) {
36863679
// Simple mock filtering - in real use, you'd use parseLoadSubsetOptions

0 commit comments

Comments
 (0)