Skip to content

Commit 04f38f4

Browse files
committed
Fix type of utils in collection options
1 parent f66f2cf commit 04f38f4

File tree

3 files changed

+137
-18
lines changed

3 files changed

+137
-18
lines changed

packages/db/src/collection/index.ts

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,32 +127,85 @@ export interface Collection<
127127
*
128128
*/
129129

130-
// Overload for when schema is provided
130+
// Overload for when schema is provided and utils is required (not optional)
131+
// We can't infer the Utils type from the CollectionConfig because it will always be optional
132+
// So we omit it from that type and instead infer it from the extension `& { utils: TUtils }`
133+
// such that we have the real, non-optional Utils type
131134
export function createCollection<
132135
T extends StandardSchemaV1,
133-
TKey extends string | number = string | number,
134-
TUtils extends UtilsRecord = UtilsRecord,
136+
TKey extends string | number,
137+
TUtils extends UtilsRecord,
135138
>(
136-
options: CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils> & {
139+
options: Omit<
140+
CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils>,
141+
`utils`
142+
> & {
137143
schema: T
138-
utils?: TUtils
144+
utils: TUtils // Required utils
139145
} & NonSingleResult
140146
): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &
141147
NonSingleResult
142148

149+
// Overload for when schema is provided and utils is optional
150+
// In this case we can simply infer the Utils type from the CollectionConfig type
151+
export function createCollection<
152+
T extends StandardSchemaV1,
153+
TKey extends string | number,
154+
TUtils extends UtilsRecord,
155+
>(
156+
options: CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils> & {
157+
schema: T
158+
} & NonSingleResult
159+
): Collection<
160+
InferSchemaOutput<T>,
161+
TKey,
162+
Exclude<TUtils, undefined>,
163+
T,
164+
InferSchemaInput<T>
165+
> &
166+
NonSingleResult
167+
168+
// Overload for when schema is provided, singleResult is true, and utils is required
169+
export function createCollection<
170+
T extends StandardSchemaV1,
171+
TKey extends string | number,
172+
TUtils extends UtilsRecord,
173+
>(
174+
options: Omit<
175+
CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils>,
176+
`utils`
177+
> & {
178+
schema: T
179+
utils: TUtils // Required utils
180+
} & SingleResult
181+
): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &
182+
SingleResult
183+
143184
// Overload for when schema is provided and singleResult is true
144185
export function createCollection<
145186
T extends StandardSchemaV1,
146-
TKey extends string | number = string | number,
147-
TUtils extends UtilsRecord = UtilsRecord,
187+
TKey extends string | number,
188+
TUtils extends UtilsRecord,
148189
>(
149190
options: CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils> & {
150191
schema: T
151-
utils?: TUtils
152192
} & SingleResult
153193
): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &
154194
SingleResult
155195

196+
// Overload for when no schema is provided and utils is required
197+
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
198+
export function createCollection<
199+
T extends object,
200+
TKey extends string | number,
201+
TUtils extends UtilsRecord,
202+
>(
203+
options: Omit<CollectionConfig<T, TKey, never, TUtils>, `utils`> & {
204+
schema?: never // prohibit schema if an explicit type is provided
205+
utils: TUtils // Required utils
206+
} & NonSingleResult
207+
): Collection<T, TKey, TUtils, never, T> & NonSingleResult
208+
156209
// Overload for when no schema is provided
157210
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
158211
export function createCollection<
@@ -162,10 +215,22 @@ export function createCollection<
162215
>(
163216
options: CollectionConfig<T, TKey, never, TUtils> & {
164217
schema?: never // prohibit schema if an explicit type is provided
165-
utils?: TUtils
166218
} & NonSingleResult
167219
): Collection<T, TKey, TUtils, never, T> & NonSingleResult
168220

221+
// Overload for when no schema is provided, singleResult is true, and utils is required
222+
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
223+
export function createCollection<
224+
T extends object,
225+
TKey extends string | number = string | number,
226+
TUtils extends UtilsRecord = UtilsRecord,
227+
>(
228+
options: Omit<CollectionConfig<T, TKey, never, TUtils>, `utils`> & {
229+
schema?: never // prohibit schema if an explicit type is provided
230+
utils: TUtils // Required utils
231+
} & SingleResult
232+
): Collection<T, TKey, TUtils, never, T> & SingleResult
233+
169234
// Overload for when no schema is provided and singleResult is true
170235
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
171236
export function createCollection<
@@ -175,15 +240,13 @@ export function createCollection<
175240
>(
176241
options: CollectionConfig<T, TKey, never, TUtils> & {
177242
schema?: never // prohibit schema if an explicit type is provided
178-
utils?: TUtils
179243
} & SingleResult
180244
): Collection<T, TKey, TUtils, never, T> & SingleResult
181245

182246
// Implementation
183247
export function createCollection(
184-
options: CollectionConfig<any, string | number, any> & {
248+
options: CollectionConfig<any, string | number, any, UtilsRecord> & {
185249
schema?: StandardSchemaV1
186-
utils?: UtilsRecord
187250
}
188251
): Collection<any, string | number, UtilsRecord, any, any> {
189252
const collection = new CollectionImpl<any, string | number, any, any, any>(

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,9 +294,9 @@ export function electricCollectionOptions<T extends StandardSchemaV1>(
294294
config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {
295295
schema: T
296296
}
297-
): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {
297+
): Omit<CollectionConfig<InferSchemaOutput<T>, string | number, T>, `utils`> & {
298298
id?: string
299-
utils: ElectricCollectionUtils
299+
utils: ElectricCollectionUtils<InferSchemaOutput<T>>
300300
schema: T
301301
}
302302

@@ -305,15 +305,15 @@ export function electricCollectionOptions<T extends Row<unknown>>(
305305
config: ElectricCollectionConfig<T> & {
306306
schema?: never // prohibit schema
307307
}
308-
): CollectionConfig<T, string | number> & {
308+
): Omit<CollectionConfig<T, string | number>, `utils`> & {
309309
id?: string
310-
utils: ElectricCollectionUtils
310+
utils: ElectricCollectionUtils<T>
311311
schema?: never // no schema in the result
312312
}
313313

314314
export function electricCollectionOptions(
315315
config: ElectricCollectionConfig<any, any>
316-
): CollectionConfig<any, string | number, any> & {
316+
): Omit<CollectionConfig<any, string | number>, `utils`> & {
317317
id?: string
318318
utils: ElectricCollectionUtils
319319
schema?: any

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

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
gt,
99
} from "@tanstack/db"
1010
import { electricCollectionOptions } from "../src/electric"
11-
import type { ElectricCollectionConfig } from "../src/electric"
11+
import type {
12+
ElectricCollectionConfig,
13+
ElectricCollectionUtils,
14+
} from "../src/electric"
1215
import type {
1316
DeleteMutationFnParams,
1417
InsertMutationFnParams,
@@ -97,6 +100,59 @@ describe(`Electric collection type resolution tests`, () => {
97100
expectTypeOf(options.getKey).parameters.toEqualTypeOf<[FallbackType]>()
98101
})
99102

103+
it(`should type collection.utils as ElectricCollectionUtils<T>`, () => {
104+
const todoSchema = z.object({
105+
id: z.string(),
106+
title: z.string(),
107+
completed: z.boolean(),
108+
})
109+
110+
type TodoType = z.infer<typeof todoSchema>
111+
112+
const options = electricCollectionOptions({
113+
id: `todos`,
114+
getKey: (item) => item.id,
115+
shapeOptions: {
116+
url: `/api/todos`,
117+
params: { table: `todos` },
118+
},
119+
schema: todoSchema,
120+
/*
121+
onInsert: async ({ collection }) => {
122+
const testCollectionUtils: ElectricCollectionUtils<TodoType> =
123+
collection.utils
124+
expectTypeOf(testCollectionUtils.awaitTxId).toBeFunction
125+
expectTypeOf(collection.utils.awaitTxId).toBeFunction
126+
return Promise.resolve({ txid: 1 })
127+
},
128+
*/
129+
})
130+
131+
// ✅ Test that options.utils is typed as ElectricCollectionUtils<TodoType>
132+
// The options object should have the correct type from electricCollectionOptions
133+
const testOptionsUtils: ElectricCollectionUtils<TodoType> = options.utils
134+
135+
expectTypeOf(testOptionsUtils.awaitTxId).toBeFunction
136+
137+
const todosCollection = createCollection(options)
138+
139+
// Test that todosCollection.utils is ElectricCollectionUtils<TodoType>
140+
// Note: We can't use expectTypeOf(...).toEqualTypeOf<ElectricCollectionUtils<T>> because
141+
// expectTypeOf's toEqualTypeOf has a constraint that requires { [x: string]: any; [x: number]: never; },
142+
// but ElectricCollectionUtils extends UtilsRecord which is Record<string, any> (no number index signature).
143+
// This causes a constraint error instead of a type mismatch error.
144+
// Instead, we test via type assignment which will show a proper type error if the types don't match.
145+
// Currently this shows that todosCollection.utils is typed as UtilsRecord, not ElectricCollectionUtils<TodoType>
146+
const testTodosUtils: ElectricCollectionUtils<TodoType> =
147+
todosCollection.utils
148+
149+
expectTypeOf(testTodosUtils.awaitTxId).toBeFunction
150+
151+
// Verify the specific properties that define ElectricCollectionUtils exist and are functions
152+
expectTypeOf(todosCollection.utils.awaitTxId).toBeFunction
153+
expectTypeOf(todosCollection.utils.awaitMatch).toBeFunction
154+
})
155+
100156
it(`should properly type the onInsert, onUpdate, and onDelete handlers`, () => {
101157
const options = electricCollectionOptions<ExplicitType>({
102158
shapeOptions: {

0 commit comments

Comments
 (0)