Skip to content

Commit a540d7c

Browse files
KyleAMathewsclaude
andauthored
Make ctx.meta.loadSubsetOptions type safe (#869)
* fix: ensure module augmentation for ctx.meta is automatically loaded Previously, the module augmentation for @tanstack/query-core that makes ctx.meta?.loadSubsetOptions type-safe was in query.ts. This meant it wasn't always processed by TypeScript unless something from that file was directly imported. This commit fixes the issue by: 1. Moving the module augmentation to a dedicated global.d.ts file 2. Adding a triple-slash reference in index.ts to ensure global.d.ts is always loaded when the package is imported 3. Removing the duplicate module augmentation from query.ts Now, ctx.meta?.loadSubsetOptions is automatically typed as LoadSubsetOptions without requiring users to explicitly import QueryCollectionMeta or add any manual type assertions. Fixes the issue where users needed to use @ts-ignore or manual type assertions to pass ctx.meta?.loadSubsetOptions to parseLoadSubsetOptions. * fix: change QueryCollectionMeta to interface for proper extensibility This commit addresses two critical issues identified in code review: 1. **Fixed double export**: Removed duplicate QueryCollectionMeta export from the query.ts line in index.ts. QueryCollectionMeta is now only exported from global.d.ts, which is its canonical source. 2. **Changed from type alias to interface**: Converting QueryCollectionMeta from a type alias to an interface enables proper TypeScript declaration merging. This allows users to extend meta with custom properties without encountering "Subsequent property declarations must have the same type" errors. The updated documentation now shows the correct pattern for users to extend meta: ```typescript declare module "@tanstack/query-db-collection" { interface QueryCollectionMeta { myCustomProperty: string } } ``` This is safer than the previous pattern which would have caused users to collide with the library's own Register.queryMeta augmentation. Added a type test documenting the extension pattern for future reference. * chore: run prettier on query.test-d.ts * fix: resolve eslint errors in query-db-collection - Replace triple-slash reference with import type {} from './global' - Remove unused ExtendedMeta interface from test All errors are now resolved, only pre-existing warnings remain. * chore: add changeset for automatic meta type-safety * docs: add section on extending meta with custom properties Added comprehensive documentation explaining: - Automatic type-safety for ctx.meta.loadSubsetOptions - How to extend QueryCollectionMeta with custom properties via module augmentation - Real-world examples including API request context - Important notes about TypeScript declaration merging This aligns with TanStack Query's official approach to typing meta using the Register interface. * fix: ensure global.ts is emitted in build output Changed global.d.ts to global.ts so TypeScript properly emits it as global.d.ts in the dist folder. TypeScript's declaration emit only generates .d.ts files from .ts source files - it does not copy handwritten .d.ts files to the output. The module augmentation for @tanstack/query-core is now guaranteed to load because: 1. global.ts exports QueryCollectionMeta interface 2. index.ts re-exports it: export type { QueryCollectionMeta } from "./global" 3. When TypeScript processes the built index.d.ts, it must load global.d.ts to resolve the export, which triggers processing of the module augmentation This follows TypeScript best practices: - Renamed .d.ts → .ts for proper build emission - Re-export forces TypeScript to process the augmentation file - No reliance on triple-slash references or side-effect imports Updated comments to accurately reflect the mechanism. --------- Co-authored-by: Claude <[email protected]>
1 parent 489ed26 commit a540d7c

File tree

6 files changed

+252
-30
lines changed

6 files changed

+252
-30
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@tanstack/query-db-collection": patch
3+
---
4+
5+
fix: ensure ctx.meta.loadSubsetOptions type-safety works automatically
6+
7+
The module augmentation for ctx.meta.loadSubsetOptions is now guaranteed to load automatically when importing from @tanstack/query-db-collection. Previously, users needed to explicitly import QueryCollectionMeta or use @ts-ignore to pass ctx.meta?.loadSubsetOptions to parseLoadSubsetOptions.
8+
9+
Additionally, QueryCollectionMeta is now an interface (instead of a type alias), enabling users to safely extend meta with custom properties via declaration merging:
10+
11+
```typescript
12+
declare module "@tanstack/query-db-collection" {
13+
interface QueryCollectionMeta {
14+
myCustomProperty: string
15+
}
16+
}
17+
```

docs/collections/query-collection.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,126 @@ The `queryCollectionOptions` function accepts the following options:
7777
- `onUpdate`: Handler called before update operations
7878
- `onDelete`: Handler called before delete operations
7979

80+
## Extending Meta with Custom Properties
81+
82+
The `meta` option allows you to pass additional metadata to your query function. By default, Query Collections automatically include `loadSubsetOptions` in the meta object, which contains filtering, sorting, and pagination options for on-demand queries.
83+
84+
### Type-Safe Meta Access
85+
86+
The `ctx.meta.loadSubsetOptions` property is automatically typed as `LoadSubsetOptions` without requiring any additional imports or type assertions:
87+
88+
```typescript
89+
import { parseLoadSubsetOptions } from "@tanstack/query-db-collection"
90+
91+
const collection = createCollection(
92+
queryCollectionOptions({
93+
queryKey: ["products"],
94+
syncMode: "on-demand",
95+
queryFn: async (ctx) => {
96+
// ✅ Type-safe access - no @ts-ignore needed!
97+
const options = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)
98+
99+
// Use the parsed options to fetch only what you need
100+
return api.getProducts(options)
101+
},
102+
queryClient,
103+
getKey: (item) => item.id,
104+
})
105+
)
106+
```
107+
108+
### Adding Custom Meta Properties
109+
110+
You can extend the meta type to include your own custom properties using TypeScript's module augmentation:
111+
112+
```typescript
113+
// In a global type definition file (e.g., types.d.ts or global.d.ts)
114+
declare module "@tanstack/query-db-collection" {
115+
interface QueryCollectionMeta {
116+
// Add your custom properties here
117+
userId?: string
118+
includeDeleted?: boolean
119+
cacheTTL?: number
120+
}
121+
}
122+
```
123+
124+
Once you've extended the interface, your custom properties are fully typed throughout your application:
125+
126+
```typescript
127+
const collection = createCollection(
128+
queryCollectionOptions({
129+
queryKey: ["todos"],
130+
queryFn: async (ctx) => {
131+
// ✅ Both loadSubsetOptions and custom properties are typed
132+
const { loadSubsetOptions, userId, includeDeleted } = ctx.meta
133+
134+
return api.getTodos({
135+
...parseLoadSubsetOptions(loadSubsetOptions),
136+
userId,
137+
includeDeleted,
138+
})
139+
},
140+
queryClient,
141+
getKey: (item) => item.id,
142+
// Pass custom meta alongside Query Collection defaults
143+
meta: {
144+
userId: "user-123",
145+
includeDeleted: false,
146+
},
147+
})
148+
)
149+
```
150+
151+
### Important Notes
152+
153+
- The module augmentation pattern follows TanStack Query's official approach for typing meta
154+
- `QueryCollectionMeta` is an interface (not a type alias), enabling proper TypeScript declaration merging
155+
- Your custom properties are merged with the base `loadSubsetOptions` property
156+
- All meta properties must be compatible with `Record<string, unknown>`
157+
- The augmentation should be done in a file that's included in your TypeScript compilation
158+
159+
### Example: API Request Context
160+
161+
A common use case is passing request context to your query function:
162+
163+
```typescript
164+
// types.d.ts
165+
declare module "@tanstack/query-db-collection" {
166+
interface QueryCollectionMeta {
167+
authToken?: string
168+
locale?: string
169+
version?: string
170+
}
171+
}
172+
173+
// collections.ts
174+
const productsCollection = createCollection(
175+
queryCollectionOptions({
176+
queryKey: ["products"],
177+
queryFn: async (ctx) => {
178+
const { loadSubsetOptions, authToken, locale, version } = ctx.meta
179+
180+
return api.getProducts({
181+
...parseLoadSubsetOptions(loadSubsetOptions),
182+
headers: {
183+
Authorization: `Bearer ${authToken}`,
184+
"Accept-Language": locale,
185+
"API-Version": version,
186+
},
187+
})
188+
},
189+
queryClient,
190+
getKey: (item) => item.id,
191+
meta: {
192+
authToken: session.token,
193+
locale: "en-US",
194+
version: "v1",
195+
},
196+
})
197+
)
198+
```
199+
80200
## Persistence Handlers
81201

82202
You can define handlers that are called when mutations occur. These handlers can persist changes to your backend and control whether the query should refetch after the operation:
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Global type augmentation for @tanstack/query-core
3+
*
4+
* This file ensures the module augmentation is always loaded when the package is imported.
5+
* The index.ts file re-exports QueryCollectionMeta from this file, which guarantees
6+
* TypeScript processes this file (and its module augmentation) whenever anyone imports
7+
* from @tanstack/query-db-collection.
8+
*
9+
* This makes ctx.meta?.loadSubsetOptions automatically type-safe without requiring
10+
* users to manually import QueryCollectionMeta.
11+
*/
12+
13+
import type { LoadSubsetOptions } from "@tanstack/db"
14+
15+
/**
16+
* Base interface for Query Collection meta properties.
17+
* Users can extend this interface to add their own custom properties while
18+
* preserving loadSubsetOptions.
19+
*
20+
* @example
21+
* ```typescript
22+
* declare module "@tanstack/query-db-collection" {
23+
* interface QueryCollectionMeta {
24+
* myCustomProperty: string
25+
* userId?: number
26+
* }
27+
* }
28+
* ```
29+
*/
30+
export interface QueryCollectionMeta extends Record<string, unknown> {
31+
loadSubsetOptions: LoadSubsetOptions
32+
}
33+
34+
// Module augmentation to extend TanStack Query's Register interface
35+
// This ensures that ctx.meta always includes loadSubsetOptions
36+
declare module "@tanstack/query-core" {
37+
interface Register {
38+
queryMeta: QueryCollectionMeta
39+
}
40+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
// Export QueryCollectionMeta from global.ts
2+
// This ensures the module augmentation in global.ts is processed by TypeScript
3+
export type { QueryCollectionMeta } from "./global"
4+
15
export {
26
queryCollectionOptions,
37
type QueryCollectionConfig,
4-
type QueryCollectionMeta,
58
type QueryCollectionUtils,
69
type SyncOperation,
710
} from "./query"

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

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,35 +31,6 @@ 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-
6334
// Schema output type inference helper (matches electric.ts pattern)
6435
type InferSchemaOutput<T> = T extends StandardSchemaV1
6536
? StandardSchemaV1.InferOutput<T> extends object

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,5 +488,76 @@ describe(`Query collection type resolution tests`, () => {
488488
const options = queryCollectionOptions(config)
489489
createCollection(options)
490490
})
491+
492+
it(`should have loadSubsetOptions typed automatically without explicit QueryCollectionMeta import`, () => {
493+
// This test validates that the module augmentation works automatically
494+
// Note: We are NOT importing QueryCollectionMeta, yet ctx.meta.loadSubsetOptions
495+
// should still be properly typed as LoadSubsetOptions
496+
const config: QueryCollectionConfig<TestItem> = {
497+
id: `autoTypeTest`,
498+
queryClient,
499+
queryKey: [`autoTypeTest`],
500+
queryFn: (ctx) => {
501+
// This should compile without errors because the module augmentation
502+
// in global.d.ts is automatically loaded via the triple-slash reference
503+
// in index.ts
504+
const options = ctx.meta?.loadSubsetOptions
505+
506+
// Verify the type is correct
507+
expectTypeOf(options).toMatchTypeOf<LoadSubsetOptions | undefined>()
508+
509+
// Verify it can be passed to parseLoadSubsetOptions without type errors
510+
const parsed = parseLoadSubsetOptions(options)
511+
expectTypeOf(parsed).toMatchTypeOf<{
512+
filters: Array<any>
513+
sorts: Array<any>
514+
limit?: number
515+
}>()
516+
517+
return Promise.resolve([])
518+
},
519+
getKey: (item) => item.id,
520+
syncMode: `on-demand`,
521+
}
522+
523+
const options = queryCollectionOptions(config)
524+
createCollection(options)
525+
})
526+
527+
it(`should allow users to extend QueryCollectionMeta via module augmentation`, () => {
528+
// This test validates that users can extend QueryCollectionMeta to add custom properties
529+
// by augmenting the @tanstack/query-db-collection module
530+
531+
// In reality, users would do:
532+
// declare module "@tanstack/query-db-collection" {
533+
// interface QueryCollectionMeta {
534+
// customUserId: number
535+
// customContext?: string
536+
// }
537+
// }
538+
539+
const config: QueryCollectionConfig<TestItem> = {
540+
id: `extendMetaTest`,
541+
queryClient,
542+
queryKey: [`extendMetaTest`],
543+
queryFn: (ctx) => {
544+
// ctx.meta still has loadSubsetOptions
545+
expectTypeOf(ctx.meta?.loadSubsetOptions).toMatchTypeOf<
546+
LoadSubsetOptions | undefined
547+
>()
548+
549+
// This test documents the extension pattern even though we can't
550+
// actually augment QueryCollectionMeta in a test file (it would
551+
// affect all other tests in the same compilation unit)
552+
553+
return Promise.resolve([])
554+
},
555+
getKey: (item) => item.id,
556+
syncMode: `on-demand`,
557+
}
558+
559+
const options = queryCollectionOptions(config)
560+
createCollection(options)
561+
})
491562
})
492563
})

0 commit comments

Comments
 (0)