Skip to content

Commit bda3f24

Browse files
authored
fix local collection types so that they infer from a passed schema (#372)
1 parent bc2f204 commit bda3f24

File tree

5 files changed

+215
-11
lines changed

5 files changed

+215
-11
lines changed

.changeset/stupid-pandas-end.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/db": patch
3+
---
4+
5+
Fix the types on `localOnlyCollectionOptions` and `localStorageCollectionOptions` so that they correctly infer the types from a passed in schema

packages/db/src/local-only.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
CollectionConfig,
23
DeleteMutationFnParams,
34
InsertMutationFnParams,
45
OperationType,
@@ -139,7 +140,11 @@ export function localOnlyCollectionOptions<
139140
TSchema extends StandardSchemaV1 = never,
140141
TFallback extends Record<string, unknown> = Record<string, unknown>,
141142
TKey extends string | number = string | number,
142-
>(config: LocalOnlyCollectionConfig<TExplicit, TSchema, TFallback, TKey>) {
143+
>(
144+
config: LocalOnlyCollectionConfig<TExplicit, TSchema, TFallback, TKey>
145+
): CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>, TKey> & {
146+
utils: LocalOnlyCollectionUtils
147+
} {
143148
type ResolvedType = ResolveType<TExplicit, TSchema, TFallback>
144149

145150
const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config

packages/db/src/local-storage.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,12 @@ export function localStorageCollectionOptions<
206206
TExplicit = unknown,
207207
TSchema extends StandardSchemaV1 = never,
208208
TFallback extends object = Record<string, unknown>,
209-
>(config: LocalStorageCollectionConfig<TExplicit, TSchema, TFallback>) {
209+
>(
210+
config: LocalStorageCollectionConfig<TExplicit, TSchema, TFallback>
211+
): Omit<CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>, `id`> & {
212+
id: string
213+
utils: LocalStorageCollectionUtils
214+
} {
210215
type ResolvedType = ResolveType<TExplicit, TSchema, TFallback>
211216

212217
// Validate required parameters

packages/db/tests/local-only.test-d.ts

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { describe, expectTypeOf, it } from "vitest"
2+
import { z } from "zod"
23
import { createCollection } from "../src/index"
34
import { localOnlyCollectionOptions } from "../src/local-only"
45
import type { LocalOnlyCollectionUtils } from "../src/local-only"
56
import type { Collection } from "../src/index"
7+
import type { Query } from "../src/query/builder"
68

79
interface TestItem extends Record<string, unknown> {
810
id: number
@@ -33,7 +35,7 @@ describe(`LocalOnly Collection Types`, () => {
3335
expectTypeOf(options).toHaveProperty(`getKey`)
3436

3537
// Test that getKey returns the correct type
36-
expectTypeOf(options.getKey).toMatchTypeOf<(item: TestItem) => number>()
38+
expectTypeOf(options.getKey).toExtend<(item: TestItem) => number>()
3739
})
3840

3941
it(`should be compatible with createCollection`, () => {
@@ -56,7 +58,7 @@ describe(`LocalOnly Collection Types`, () => {
5658
>(options)
5759

5860
// Test that the collection has the expected type
59-
expectTypeOf(collection).toMatchTypeOf<
61+
expectTypeOf(collection).toExtend<
6062
Collection<TestItem, number, LocalOnlyCollectionUtils>
6163
>()
6264
})
@@ -82,7 +84,7 @@ describe(`LocalOnly Collection Types`, () => {
8284
LocalOnlyCollectionUtils
8385
>(options)
8486

85-
expectTypeOf(collection).toMatchTypeOf<
87+
expectTypeOf(collection).toExtend<
8688
Collection<TestItem, number, LocalOnlyCollectionUtils>
8789
>()
8890
})
@@ -106,7 +108,7 @@ describe(`LocalOnly Collection Types`, () => {
106108
LocalOnlyCollectionUtils
107109
>(options)
108110

109-
expectTypeOf(collection).toMatchTypeOf<
111+
expectTypeOf(collection).toExtend<
110112
Collection<TestItem, number, LocalOnlyCollectionUtils>
111113
>()
112114
})
@@ -129,9 +131,130 @@ describe(`LocalOnly Collection Types`, () => {
129131
LocalOnlyCollectionUtils
130132
>(options)
131133

132-
expectTypeOf(collection).toMatchTypeOf<
134+
expectTypeOf(collection).toExtend<
133135
Collection<TestItem, string, LocalOnlyCollectionUtils>
134136
>()
135-
expectTypeOf(options.getKey).toMatchTypeOf<(item: TestItem) => string>()
137+
expectTypeOf(options.getKey).toExtend<(item: TestItem) => string>()
138+
})
139+
140+
it(`should work with schema and infer correct types`, () => {
141+
const testSchema = z.object({
142+
id: z.string(),
143+
entityId: z.string(),
144+
value: z.string(),
145+
})
146+
147+
const config = {
148+
id: `test-with-schema`,
149+
getKey: (item: any) => item.id,
150+
schema: testSchema,
151+
}
152+
153+
const options = localOnlyCollectionOptions(config)
154+
const collection = createCollection(options)
155+
156+
// Test that the collection has the correct inferred type from schema
157+
expectTypeOf(collection).toExtend<
158+
Collection<
159+
{
160+
id: string
161+
entityId: string
162+
value: string
163+
},
164+
string,
165+
LocalOnlyCollectionUtils
166+
>
167+
>()
168+
})
169+
170+
it(`should work with schema and query builder type inference (bug report reproduction)`, () => {
171+
const testSchema = z.object({
172+
id: z.string(),
173+
entityId: z.string(),
174+
value: z.string(),
175+
createdAt: z.date(),
176+
})
177+
178+
const config = {
179+
id: `test-with-schema-query`,
180+
getKey: (item: any) => item.id,
181+
schema: testSchema,
182+
}
183+
184+
const options = localOnlyCollectionOptions(config)
185+
const collection = createCollection(options)
186+
187+
// This should work without type errors - the query builder should infer the correct type
188+
const query = (q: InstanceType<typeof Query>) =>
189+
q
190+
.from({ bookmark: collection })
191+
.orderBy(({ bookmark }) => bookmark.createdAt, `desc`)
192+
193+
// Test that the collection has the correct inferred type from schema
194+
expectTypeOf(collection).toExtend<
195+
Collection<
196+
{
197+
id: string
198+
entityId: string
199+
value: string
200+
createdAt: Date
201+
},
202+
string,
203+
LocalOnlyCollectionUtils
204+
>
205+
>()
206+
207+
// Test that the query builder can access the createdAt property
208+
expectTypeOf(query).toBeFunction()
209+
})
210+
211+
it(`should reproduce exact bug report scenario`, () => {
212+
// This reproduces the exact scenario from the bug report
213+
const selectUrlSchema = z.object({
214+
id: z.string(),
215+
url: z.string(),
216+
title: z.string(),
217+
createdAt: z.date(),
218+
})
219+
220+
const initialData = [
221+
{
222+
id: `1`,
223+
url: `https://example.com`,
224+
title: `Example`,
225+
createdAt: new Date(),
226+
},
227+
]
228+
229+
const bookmarkCollection = createCollection(
230+
localOnlyCollectionOptions({
231+
initialData,
232+
getKey: (url: any) => url.id,
233+
schema: selectUrlSchema,
234+
})
235+
)
236+
237+
// This should work without type errors - the query builder should infer the correct type
238+
const query = (q: InstanceType<typeof Query>) =>
239+
q
240+
.from({ bookmark: bookmarkCollection })
241+
.orderBy(({ bookmark }) => bookmark.createdAt, `desc`)
242+
243+
// Test that the collection has the correct inferred type from schema
244+
expectTypeOf(bookmarkCollection).toExtend<
245+
Collection<
246+
{
247+
id: string
248+
url: string
249+
title: string
250+
createdAt: Date
251+
},
252+
string,
253+
LocalOnlyCollectionUtils
254+
>
255+
>()
256+
257+
// Test that the query builder can access the createdAt property
258+
expectTypeOf(query).toBeFunction()
136259
})
137260
})

packages/db/tests/local-storage.test-d.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expectTypeOf, it } from "vitest"
22
import { z } from "zod"
33
import { createCollection } from "../src/index"
44
import { localStorageCollectionOptions } from "../src/local-storage"
5+
import type { Query } from "../src/query/builder"
56
import type {
67
LocalStorageCollectionConfig,
78
StorageApi,
@@ -227,7 +228,7 @@ describe(`LocalStorage collection type resolution tests`, () => {
227228
})
228229

229230
// Verify sync has the correct type and optional getSyncMetadata
230-
expectTypeOf(options.sync).toMatchTypeOf<
231+
expectTypeOf(options.sync).toExtend<
231232
CollectionConfig<ExplicitType>[`sync`]
232233
>()
233234

@@ -276,7 +277,72 @@ describe(`LocalStorage collection type resolution tests`, () => {
276277
}
277278

278279
// These should be assignable to our interfaces
279-
expectTypeOf(localStorage).toMatchTypeOf<StorageApi>()
280-
expectTypeOf(windowEventApi).toMatchTypeOf<StorageEventApi>()
280+
expectTypeOf(localStorage).toExtend<StorageApi>()
281+
expectTypeOf(windowEventApi).toExtend<StorageEventApi>()
282+
})
283+
284+
it(`should work with schema and query builder type inference (bug report reproduction)`, () => {
285+
const queryTestSchema = z.object({
286+
id: z.string(),
287+
entityId: z.string(),
288+
value: z.string(),
289+
createdAt: z.date(),
290+
})
291+
292+
const config = {
293+
storageKey: `test-with-schema-query`,
294+
storage: mockStorage,
295+
storageEventApi: mockStorageEventApi,
296+
getKey: (item: any) => item.id,
297+
schema: queryTestSchema,
298+
}
299+
300+
const options = localStorageCollectionOptions(config)
301+
const collection = createCollection(options)
302+
303+
// This should work without type errors - the query builder should infer the correct type
304+
const query = (q: InstanceType<typeof Query>) =>
305+
q
306+
.from({ bookmark: collection })
307+
.orderBy(({ bookmark }) => bookmark.createdAt, `desc`)
308+
309+
// Test that the collection has the correct inferred type from schema
310+
expectTypeOf(collection).toExtend<any>() // Using any here since we don't have the exact Collection type imported
311+
312+
// Test that the query builder can access the createdAt property
313+
expectTypeOf(query).toBeFunction()
314+
})
315+
316+
it(`should reproduce exact bug report scenario with localStorage`, () => {
317+
// This reproduces the exact scenario from the bug report but with localStorage
318+
const selectUrlSchema = z.object({
319+
id: z.string(),
320+
url: z.string(),
321+
title: z.string(),
322+
createdAt: z.date(),
323+
})
324+
325+
const config = {
326+
storageKey: `test-with-schema`,
327+
storage: mockStorage,
328+
storageEventApi: mockStorageEventApi,
329+
getKey: (url: any) => url.id,
330+
schema: selectUrlSchema,
331+
}
332+
333+
const options = localStorageCollectionOptions(config)
334+
const collection = createCollection(options)
335+
336+
// This should work without type errors - the query builder should infer the correct type
337+
const query = (q: InstanceType<typeof Query>) =>
338+
q
339+
.from({ bookmark: collection })
340+
.orderBy(({ bookmark }) => bookmark.createdAt, `desc`)
341+
342+
// Test that the collection has the correct inferred type from schema
343+
expectTypeOf(collection).toExtend<any>() // Using any here since we don't have the exact Collection type imported
344+
345+
// Test that the query builder can access the createdAt property
346+
expectTypeOf(query).toBeFunction()
281347
})
282348
})

0 commit comments

Comments
 (0)