From b69c03f3083a3f3bf1f74d7f75527893d5a3676d Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 09:32:09 +0100 Subject: [PATCH 01/14] Proprly type collection utils for Electric, Trailbase, PowerSync, and local collections. --- packages/db/src/local-only.ts | 2 +- packages/electric-db-collection/src/electric.ts | 4 ++-- packages/powersync-db-collection/src/definitions.ts | 2 +- packages/trailbase-db-collection/src/trailbase.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/db/src/local-only.ts b/packages/db/src/local-only.ts index 68168ae02..642e286d7 100644 --- a/packages/db/src/local-only.ts +++ b/packages/db/src/local-only.ts @@ -64,7 +64,7 @@ type LocalOnlyCollectionOptionsResult< T extends object, TKey extends string | number, TSchema extends StandardSchemaV1 | never = never, -> = CollectionConfig & { +> = CollectionConfig & { utils: LocalOnlyCollectionUtils } diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index 25bf29e53..5c97b0411 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -511,7 +511,7 @@ export function electricCollectionOptions( config: ElectricCollectionConfig, T> & { schema: T }, -): Omit, string | number, T>, `utils`> & { +): Omit, string | number, T, ElectricCollectionUtils>>, `utils`> & { id?: string utils: ElectricCollectionUtils> schema: T @@ -522,7 +522,7 @@ export function electricCollectionOptions>( config: ElectricCollectionConfig & { schema?: never // prohibit schema }, -): Omit, `utils`> & { +): Omit>, `utils`> & { id?: string utils: ElectricCollectionUtils schema?: never // no schema in the result diff --git a/packages/powersync-db-collection/src/definitions.ts b/packages/powersync-db-collection/src/definitions.ts index c65fa850e..db1eda0a5 100644 --- a/packages/powersync-db-collection/src/definitions.ts +++ b/packages/powersync-db-collection/src/definitions.ts @@ -260,7 +260,7 @@ export type EnhancedPowerSyncCollectionConfig< TTable extends Table, OutputType extends Record = Record, TSchema extends StandardSchemaV1 = never, -> = CollectionConfig & { +> = CollectionConfig> & { id?: string utils: PowerSyncCollectionUtils schema?: TSchema diff --git a/packages/trailbase-db-collection/src/trailbase.ts b/packages/trailbase-db-collection/src/trailbase.ts index 32b6755c5..517749800 100644 --- a/packages/trailbase-db-collection/src/trailbase.ts +++ b/packages/trailbase-db-collection/src/trailbase.ts @@ -113,7 +113,7 @@ export function trailBaseCollectionOptions< TKey extends string | number = string | number, >( config: TrailBaseCollectionConfig, -): CollectionConfig & { utils: TrailBaseCollectionUtils } { +): CollectionConfig & { utils: TrailBaseCollectionUtils } { const getKey = config.getKey const parse = (record: TRecord) => From 6e52850bb82716663673298fddc454de083c59a0 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 09:33:14 +0100 Subject: [PATCH 02/14] Changeset --- .changeset/spicy-nails-dream.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/spicy-nails-dream.md diff --git a/.changeset/spicy-nails-dream.md b/.changeset/spicy-nails-dream.md new file mode 100644 index 000000000..15ee424ca --- /dev/null +++ b/.changeset/spicy-nails-dream.md @@ -0,0 +1,8 @@ +--- +'@tanstack/powersync-db-collection': patch +'@tanstack/trailbase-db-collection': patch +'@tanstack/electric-db-collection': patch +'@tanstack/db': patch +--- + +Make type of collection utils more precise for local, Electric, Trailbase, and PowerSync collections. From 90cc5dbe19246b79c6ce6bc8f7b0973b9809db49 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:05:53 +0000 Subject: [PATCH 03/14] ci: apply automated fixes --- packages/electric-db-collection/src/electric.ts | 15 +++++++++++++-- .../powersync-db-collection/src/definitions.ts | 7 ++++++- packages/trailbase-db-collection/src/trailbase.ts | 4 +++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index 5c97b0411..e0fac363b 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -511,7 +511,15 @@ export function electricCollectionOptions( config: ElectricCollectionConfig, T> & { schema: T }, -): Omit, string | number, T, ElectricCollectionUtils>>, `utils`> & { +): Omit< + CollectionConfig< + InferSchemaOutput, + string | number, + T, + ElectricCollectionUtils> + >, + `utils` +> & { id?: string utils: ElectricCollectionUtils> schema: T @@ -522,7 +530,10 @@ export function electricCollectionOptions>( config: ElectricCollectionConfig & { schema?: never // prohibit schema }, -): Omit>, `utils`> & { +): Omit< + CollectionConfig>, + `utils` +> & { id?: string utils: ElectricCollectionUtils schema?: never // no schema in the result diff --git a/packages/powersync-db-collection/src/definitions.ts b/packages/powersync-db-collection/src/definitions.ts index db1eda0a5..1a605d077 100644 --- a/packages/powersync-db-collection/src/definitions.ts +++ b/packages/powersync-db-collection/src/definitions.ts @@ -260,7 +260,12 @@ export type EnhancedPowerSyncCollectionConfig< TTable extends Table, OutputType extends Record = Record, TSchema extends StandardSchemaV1 = never, -> = CollectionConfig> & { +> = CollectionConfig< + OutputType, + string, + TSchema, + PowerSyncCollectionUtils +> & { id?: string utils: PowerSyncCollectionUtils schema?: TSchema diff --git a/packages/trailbase-db-collection/src/trailbase.ts b/packages/trailbase-db-collection/src/trailbase.ts index 517749800..4d6ed078d 100644 --- a/packages/trailbase-db-collection/src/trailbase.ts +++ b/packages/trailbase-db-collection/src/trailbase.ts @@ -113,7 +113,9 @@ export function trailBaseCollectionOptions< TKey extends string | number = string | number, >( config: TrailBaseCollectionConfig, -): CollectionConfig & { utils: TrailBaseCollectionUtils } { +): CollectionConfig & { + utils: TrailBaseCollectionUtils +} { const getKey = config.getKey const parse = (record: TRecord) => From b98266f43d570f693138b0b63e184e8a8c5b8cdd Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 10:49:45 +0100 Subject: [PATCH 04/14] Revert unnecessary change to Electric collection typing --- packages/electric-db-collection/src/electric.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index e0fac363b..25bf29e53 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -511,15 +511,7 @@ export function electricCollectionOptions( config: ElectricCollectionConfig, T> & { schema: T }, -): Omit< - CollectionConfig< - InferSchemaOutput, - string | number, - T, - ElectricCollectionUtils> - >, - `utils` -> & { +): Omit, string | number, T>, `utils`> & { id?: string utils: ElectricCollectionUtils> schema: T @@ -530,10 +522,7 @@ export function electricCollectionOptions>( config: ElectricCollectionConfig & { schema?: never // prohibit schema }, -): Omit< - CollectionConfig>, - `utils` -> & { +): Omit, `utils`> & { id?: string utils: ElectricCollectionUtils schema?: never // no schema in the result From 08082917e8e4e1ce8c49726d2edd3de0d866aa0c Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 11:03:05 +0100 Subject: [PATCH 05/14] Add type tests to check the type of the utils of the collections --- packages/db/tests/local-only.test-d.ts | 15 ++++++++++ packages/db/tests/local-storage.test-d.ts | 20 +++++++++++++ .../tests/powersync.test-d.ts | 30 +++++++++++++++++++ .../query-db-collection/tests/query.test-d.ts | 22 ++++++++++++++ .../tests/trailbase.test-d.ts | 29 ++++++++++++++++++ 5 files changed, 116 insertions(+) create mode 100644 packages/powersync-db-collection/tests/powersync.test-d.ts create mode 100644 packages/trailbase-db-collection/tests/trailbase.test-d.ts diff --git a/packages/db/tests/local-only.test-d.ts b/packages/db/tests/local-only.test-d.ts index 471a266b3..f8cfe0964 100644 --- a/packages/db/tests/local-only.test-d.ts +++ b/packages/db/tests/local-only.test-d.ts @@ -2,6 +2,7 @@ import { describe, expectTypeOf, it } from 'vitest' import { z } from 'zod' import { createCollection } from '../src/index' import { localOnlyCollectionOptions } from '../src/local-only' +import type { LocalOnlyCollectionUtils } from '../src/local-only' interface TestItem extends Record { id: number @@ -252,4 +253,18 @@ describe(`LocalOnly Collection Types`, () => { // Test that the collection has the correct inferred type from schema expectTypeOf(collection.toArray).toEqualTypeOf>() }) + + it(`should type collection.utils as LocalOnlyCollectionUtils`, () => { + const collection = createCollection( + localOnlyCollectionOptions({ + id: `test-utils-typing`, + getKey: (item: TestItem) => item.id, + }), + ) + + // Verify that collection.utils is typed as LocalOnlyCollectionUtils, not UtilsRecord + const utils: LocalOnlyCollectionUtils = collection.utils + expectTypeOf(utils.acceptMutations).toBeFunction() + expectTypeOf(collection.utils.acceptMutations).toBeFunction() + }) }) diff --git a/packages/db/tests/local-storage.test-d.ts b/packages/db/tests/local-storage.test-d.ts index 3321b2e66..fb008a1e1 100644 --- a/packages/db/tests/local-storage.test-d.ts +++ b/packages/db/tests/local-storage.test-d.ts @@ -4,6 +4,7 @@ import { createCollection } from '../src/index' import { localStorageCollectionOptions } from '../src/local-storage' import type { LocalStorageCollectionConfig, + LocalStorageCollectionUtils, StorageApi, StorageEventApi, } from '../src/local-storage' @@ -394,4 +395,23 @@ describe(`LocalStorage collection type resolution tests`, () => { expectTypeOf(draft).toEqualTypeOf() }) }) + + it(`should type collection.utils as LocalStorageCollectionUtils after createCollection`, () => { + const collection = createCollection( + localStorageCollectionOptions({ + storageKey: `test-utils-typing`, + storage: mockStorage, + storageEventApi: mockStorageEventApi, + getKey: (item) => item.id, + }), + ) + + // Verify that collection.utils is typed as LocalStorageCollectionUtils, not UtilsRecord + const utils: LocalStorageCollectionUtils = collection.utils + expectTypeOf(utils.clearStorage).toBeFunction() + expectTypeOf(utils.getStorageSize).toBeFunction() + expectTypeOf(utils.acceptMutations).toBeFunction() + expectTypeOf(collection.utils.clearStorage).toBeFunction() + expectTypeOf(collection.utils.getStorageSize).toBeFunction() + }) }) diff --git a/packages/powersync-db-collection/tests/powersync.test-d.ts b/packages/powersync-db-collection/tests/powersync.test-d.ts new file mode 100644 index 000000000..ef61c84be --- /dev/null +++ b/packages/powersync-db-collection/tests/powersync.test-d.ts @@ -0,0 +1,30 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { Schema, Table, column } from '@powersync/node' +import { createCollection } from '@tanstack/db' +import { powerSyncCollectionOptions } from '../src' +import type { PowerSyncCollectionUtils } from '../src' +import type { AbstractPowerSyncDatabase } from '@powersync/node' + +const APP_SCHEMA = new Schema({ + documents: new Table({ + name: column.text, + author: column.text, + }), +}) + +describe(`PowerSync collection type tests`, () => { + it(`should type collection.utils as PowerSyncCollectionUtils after createCollection`, () => { + const collection = createCollection( + powerSyncCollectionOptions({ + database: {} as AbstractPowerSyncDatabase, + table: APP_SCHEMA.props.documents, + }), + ) + + // Verify that collection.utils is typed as PowerSyncCollectionUtils, not UtilsRecord + const utils: PowerSyncCollectionUtils<(typeof APP_SCHEMA.props)['documents']> = + collection.utils + expectTypeOf(utils.getMeta).toBeFunction() + expectTypeOf(collection.utils.getMeta).toBeFunction() + }) +}) diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index 8481021eb..6dbd233e7 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -560,4 +560,26 @@ describe(`Query collection type resolution tests`, () => { createCollection(options) }) }) + + it(`should type collection.utils as QueryCollectionUtils after createCollection`, () => { + const collection = createCollection( + queryCollectionOptions({ + id: `test-utils-typing`, + queryClient, + queryKey: [`test-utils`], + queryFn: () => Promise.resolve([]), + getKey: (item) => item.id, + }), + ) + + // Verify that collection.utils is typed as QueryCollectionUtils, not UtilsRecord + const utils: QueryCollectionUtils = collection.utils + expectTypeOf(utils.refetch).toBeFunction() + expectTypeOf(collection.utils.refetch).toBeFunction() + expectTypeOf(collection.utils.writeInsert).toBeFunction() + expectTypeOf(collection.utils.writeUpdate).toBeFunction() + expectTypeOf(collection.utils.writeDelete).toBeFunction() + expectTypeOf(collection.utils.isFetching).toBeBoolean() + expectTypeOf(collection.utils.isLoading).toBeBoolean() + }) }) diff --git a/packages/trailbase-db-collection/tests/trailbase.test-d.ts b/packages/trailbase-db-collection/tests/trailbase.test-d.ts new file mode 100644 index 000000000..c4b18a2e5 --- /dev/null +++ b/packages/trailbase-db-collection/tests/trailbase.test-d.ts @@ -0,0 +1,29 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { createCollection } from '@tanstack/db' +import { trailBaseCollectionOptions } from '../src/trailbase' +import type { TrailBaseCollectionUtils } from '../src/trailbase' +import type { RecordApi } from 'trailbase' + +type TestItem = { + id: string + title: string +} + +describe(`TrailBase collection type tests`, () => { + it(`should type collection.utils as TrailBaseCollectionUtils after createCollection`, () => { + const collection = createCollection( + trailBaseCollectionOptions({ + id: `todos`, + recordApi: {} as RecordApi, + getKey: (item) => item.id, + parse: {}, + serialize: {}, + }), + ) + + // Verify that collection.utils is typed as TrailBaseCollectionUtils, not UtilsRecord + const utils: TrailBaseCollectionUtils = collection.utils + expectTypeOf(utils.cancel).toBeFunction() + expectTypeOf(collection.utils.cancel).toBeFunction() + }) +}) From 3450ba1f70884a9f5018c07a416826a68236bbc3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:04:54 +0000 Subject: [PATCH 06/14] ci: apply automated fixes --- packages/powersync-db-collection/tests/powersync.test-d.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/powersync-db-collection/tests/powersync.test-d.ts b/packages/powersync-db-collection/tests/powersync.test-d.ts index ef61c84be..9694ec46f 100644 --- a/packages/powersync-db-collection/tests/powersync.test-d.ts +++ b/packages/powersync-db-collection/tests/powersync.test-d.ts @@ -22,8 +22,9 @@ describe(`PowerSync collection type tests`, () => { ) // Verify that collection.utils is typed as PowerSyncCollectionUtils, not UtilsRecord - const utils: PowerSyncCollectionUtils<(typeof APP_SCHEMA.props)['documents']> = - collection.utils + const utils: PowerSyncCollectionUtils< + (typeof APP_SCHEMA.props)['documents'] + > = collection.utils expectTypeOf(utils.getMeta).toBeFunction() expectTypeOf(collection.utils.getMeta).toBeFunction() }) From 1d92a512315ba058a0cc2393848dad60234a25d0 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 11:19:34 +0100 Subject: [PATCH 07/14] Fix types in powersync tests --- .../tests/powersync.test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/powersync-db-collection/tests/powersync.test.ts b/packages/powersync-db-collection/tests/powersync.test.ts index 4a4a5a544..17920f5d8 100644 --- a/packages/powersync-db-collection/tests/powersync.test.ts +++ b/packages/powersync-db-collection/tests/powersync.test.ts @@ -203,7 +203,9 @@ describe(`PowerSync Integration`, () => { const _crudEntries = await db.getAll(` SELECT * FROM ps_crud ORDER BY id`) - const crudEntries = _crudEntries.map((r) => CrudEntry.fromRow(r)) + const crudEntries = _crudEntries.map((r) => + CrudEntry.fromRow(r as Parameters[0]), + ) expect(crudEntries.length).toBe(6) // We can only group transactions for similar operations @@ -250,7 +252,9 @@ describe(`PowerSync Integration`, () => { // There should be a crud entries for this const _crudEntries = await db.getAll(` SELECT * FROM ps_crud ORDER BY id`) - const crudEntries = _crudEntries.map((r) => CrudEntry.fromRow(r)) + const crudEntries = _crudEntries.map((r) => + CrudEntry.fromRow(r as Parameters[0]), + ) const lastTransactionId = crudEntries[crudEntries.length - 1]?.transactionId @@ -312,7 +316,9 @@ describe(`PowerSync Integration`, () => { // There should be a crud entries for this const _crudEntries = await db.getAll(` SELECT * FROM ps_crud ORDER BY id`) - const crudEntries = _crudEntries.map((r) => CrudEntry.fromRow(r)) + const crudEntries = _crudEntries.map((r) => + CrudEntry.fromRow(r as Parameters[0]), + ) const lastTransactionId = crudEntries[crudEntries.length - 1]?.transactionId @@ -464,7 +470,7 @@ describe(`PowerSync Integration`, () => { liveDocuments.subscribeChanges((changes) => { changes .map((change) => change.value.name) - .forEach((change) => bookNames.add(change)) + .forEach((change) => bookNames.add(change!)) }) await collection.insert({ From fbccf98f37a0d3fe1f2dd2fa72438d7929611625 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 11:31:26 +0100 Subject: [PATCH 08/14] Fix type in trailbase type test --- .../tests/trailbase.test-d.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/trailbase-db-collection/tests/trailbase.test-d.ts b/packages/trailbase-db-collection/tests/trailbase.test-d.ts index c4b18a2e5..0598aed44 100644 --- a/packages/trailbase-db-collection/tests/trailbase.test-d.ts +++ b/packages/trailbase-db-collection/tests/trailbase.test-d.ts @@ -12,13 +12,13 @@ type TestItem = { describe(`TrailBase collection type tests`, () => { it(`should type collection.utils as TrailBaseCollectionUtils after createCollection`, () => { const collection = createCollection( - trailBaseCollectionOptions({ - id: `todos`, - recordApi: {} as RecordApi, - getKey: (item) => item.id, - parse: {}, - serialize: {}, - }), + trailBaseCollectionOptions ({ + id: `todos`, + recordApi: {} as RecordApi, + getKey: (item) => item.id, + parse: {}, + serialize: {}, + }), ) // Verify that collection.utils is typed as TrailBaseCollectionUtils, not UtilsRecord From ec999fdce6671410a92805a7378316204db64c81 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:32:30 +0000 Subject: [PATCH 09/14] ci: apply automated fixes --- .../tests/trailbase.test-d.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/trailbase-db-collection/tests/trailbase.test-d.ts b/packages/trailbase-db-collection/tests/trailbase.test-d.ts index 0598aed44..49c26ad2f 100644 --- a/packages/trailbase-db-collection/tests/trailbase.test-d.ts +++ b/packages/trailbase-db-collection/tests/trailbase.test-d.ts @@ -12,13 +12,13 @@ type TestItem = { describe(`TrailBase collection type tests`, () => { it(`should type collection.utils as TrailBaseCollectionUtils after createCollection`, () => { const collection = createCollection( - trailBaseCollectionOptions ({ - id: `todos`, - recordApi: {} as RecordApi, - getKey: (item) => item.id, - parse: {}, - serialize: {}, - }), + trailBaseCollectionOptions({ + id: `todos`, + recordApi: {} as RecordApi, + getKey: (item) => item.id, + parse: {}, + serialize: {}, + }), ) // Verify that collection.utils is typed as TrailBaseCollectionUtils, not UtilsRecord From c798b498a959955ceddcf6ef790d085eb068d2f9 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 11:40:46 +0100 Subject: [PATCH 10/14] Changeset update --- .changeset/spicy-nails-dream.md | 8 -------- .changeset/tough-dragons-fold.md | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 .changeset/spicy-nails-dream.md create mode 100644 .changeset/tough-dragons-fold.md diff --git a/.changeset/spicy-nails-dream.md b/.changeset/spicy-nails-dream.md deleted file mode 100644 index 15ee424ca..000000000 --- a/.changeset/spicy-nails-dream.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@tanstack/powersync-db-collection': patch -'@tanstack/trailbase-db-collection': patch -'@tanstack/electric-db-collection': patch -'@tanstack/db': patch ---- - -Make type of collection utils more precise for local, Electric, Trailbase, and PowerSync collections. diff --git a/.changeset/tough-dragons-fold.md b/.changeset/tough-dragons-fold.md new file mode 100644 index 000000000..cac52eab6 --- /dev/null +++ b/.changeset/tough-dragons-fold.md @@ -0,0 +1,7 @@ +--- +'@tanstack/powersync-db-collection': patch +'@tanstack/trailbase-db-collection': patch +'@tanstack/db': patch +--- + +Make type of collection utils more precise for localOnly, PowerSync, and Trailbase collections From 7855cc90356d3b01c57406ed8411bb2fea1a36d8 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 16:26:57 +0100 Subject: [PATCH 11/14] Add type test verifying utils retain concrete types after createCollection Adds a reproduction test for the issue where ElectricCollectionUtils type is widened to UtilsRecord after passing electricCollectionOptions through createCollection when handlers (onInsert, onUpdate, onDelete) are present. Co-Authored-By: Claude Opus 4.6 --- .../tests/electric.test-d.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/electric-db-collection/tests/electric.test-d.ts b/packages/electric-db-collection/tests/electric.test-d.ts index 946e6496a..27f7a967a 100644 --- a/packages/electric-db-collection/tests/electric.test-d.ts +++ b/packages/electric-db-collection/tests/electric.test-d.ts @@ -165,6 +165,44 @@ describe(`Electric collection type resolution tests`, () => { expectTypeOf(todosCollection.utils.awaitMatch).toBeFunction }) + it(`should preserve ElectricCollectionUtils type on collection.utils after createCollection with handlers`, () => { + const todoSchema = z.object({ + id: z.string(), + title: z.string(), + completed: z.boolean(), + }) + + type TodoType = z.infer + + const options = electricCollectionOptions({ + shapeOptions: { + url: `/api/todos`, + }, + schema: todoSchema, + getKey: (item) => item.id, + onInsert: async () => { + return Promise.resolve({ txid: 1 }) + }, + onUpdate: async () => { + return Promise.resolve({ txid: 1 }) + }, + onDelete: async () => { + return Promise.resolve({ txid: 1 }) + }, + }) + + const todosCollection = createCollection(options) + + // After createCollection, utils should be typed as ElectricCollectionUtils + // and not widened to UtilsRecord + const testUtils: ElectricCollectionUtils = todosCollection.utils + + expectTypeOf(testUtils.awaitTxId).toBeFunction + expectTypeOf(testUtils.awaitMatch).toBeFunction + expectTypeOf(todosCollection.utils.awaitTxId).toBeFunction + expectTypeOf(todosCollection.utils.awaitMatch).toBeFunction + }) + it(`should properly type the onInsert, onUpdate, and onDelete handlers`, () => { const options = electricCollectionOptions({ shapeOptions: { From ad291343b3000c03f8d671f494bad509bdce8a5d Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 17:07:07 +0100 Subject: [PATCH 12/14] Fix ElectricCollectionUtils type lost after createCollection Omit onInsert/onUpdate/onDelete from CollectionConfig in the return type of electricCollectionOptions overloads, then re-add them via Pick from ElectricCollectionConfig which carries the correct ElectricCollectionUtils. This eliminates conflicting TUtils inference sites when the result is passed to createCollection. Co-Authored-By: Claude Opus 4.6 --- packages/electric-db-collection/src/electric.ts | 6 ++++-- .../tests/electric.test-d.ts | 17 ++++++++--------- .../tests/electric.test.ts | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index 25bf29e53..780e906fa 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -511,7 +511,8 @@ export function electricCollectionOptions( config: ElectricCollectionConfig, T> & { schema: T }, -): Omit, string | number, T>, `utils`> & { +): Omit, string | number, T>, `utils` | `onInsert` | `onUpdate` | `onDelete`> & + Pick, T>, `onInsert` | `onUpdate` | `onDelete`> & { id?: string utils: ElectricCollectionUtils> schema: T @@ -522,7 +523,8 @@ export function electricCollectionOptions>( config: ElectricCollectionConfig & { schema?: never // prohibit schema }, -): Omit, `utils`> & { +): Omit, `utils` | `onInsert` | `onUpdate` | `onDelete`> & + Pick, `onInsert` | `onUpdate` | `onDelete`> & { id?: string utils: ElectricCollectionUtils schema?: never // no schema in the result diff --git a/packages/electric-db-collection/tests/electric.test-d.ts b/packages/electric-db-collection/tests/electric.test-d.ts index 27f7a967a..d951546a9 100644 --- a/packages/electric-db-collection/tests/electric.test-d.ts +++ b/packages/electric-db-collection/tests/electric.test-d.ts @@ -154,7 +154,6 @@ describe(`Electric collection type resolution tests`, () => { // but ElectricCollectionUtils extends UtilsRecord which is Record (no number index signature). // This causes a constraint error instead of a type mismatch error. // Instead, we test via type assignment which will show a proper type error if the types don't match. - // Currently this shows that todosCollection.utils is typed as UtilsRecord, not ElectricCollectionUtils const testTodosUtils: ElectricCollectionUtils = todosCollection.utils @@ -233,17 +232,17 @@ describe(`Electric collection type resolution tests`, () => { }, }) - // Verify that the handlers are properly typed + // Verify that the handlers are properly typed with ElectricCollectionUtils expectTypeOf(options.onInsert).parameters.toEqualTypeOf< - [InsertMutationFnParams] + [InsertMutationFnParams>] >() expectTypeOf(options.onUpdate).parameters.toEqualTypeOf< - [UpdateMutationFnParams] + [UpdateMutationFnParams>] >() expectTypeOf(options.onDelete).parameters.toEqualTypeOf< - [DeleteMutationFnParams] + [DeleteMutationFnParams>] >() }) @@ -317,15 +316,15 @@ describe(`Electric collection type resolution tests`, () => { }, }) - // Verify that the handlers are properly typed + // Verify that the handlers are properly typed with ElectricCollectionUtils expectTypeOf(options.onDelete).parameters.toEqualTypeOf< - [DeleteMutationFnParams] + [DeleteMutationFnParams>] >() expectTypeOf(options.onInsert).parameters.toEqualTypeOf< - [InsertMutationFnParams] + [InsertMutationFnParams>] >() expectTypeOf(options.onUpdate).parameters.toEqualTypeOf< - [UpdateMutationFnParams] + [UpdateMutationFnParams>] >() }) diff --git a/packages/electric-db-collection/tests/electric.test.ts b/packages/electric-db-collection/tests/electric.test.ts index 7a8bc7384..ce6af8b55 100644 --- a/packages/electric-db-collection/tests/electric.test.ts +++ b/packages/electric-db-collection/tests/electric.test.ts @@ -528,7 +528,7 @@ describe(`Electric Integration`, () => { id: `test-transaction`, mutations: [], } as unknown as TransactionWithMutations - const mockParams: InsertMutationFnParams = { + const mockParams: InsertMutationFnParams> = { transaction: mockTransaction, // @ts-expect-error not relevant to test collection: CollectionImpl, From 18f530aba4264b08fee04286d60dde331f44d8c8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:08:10 +0000 Subject: [PATCH 13/14] ci: apply automated fixes --- .../electric-db-collection/src/electric.ts | 31 +++++++----- .../tests/electric.test-d.ts | 48 ++++++++++++++++--- .../tests/electric.test.ts | 6 ++- 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index 780e906fa..261069db9 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -511,24 +511,33 @@ export function electricCollectionOptions( config: ElectricCollectionConfig, T> & { schema: T }, -): Omit, string | number, T>, `utils` | `onInsert` | `onUpdate` | `onDelete`> & - Pick, T>, `onInsert` | `onUpdate` | `onDelete`> & { - id?: string - utils: ElectricCollectionUtils> - schema: T -} +): Omit< + CollectionConfig, string | number, T>, + `utils` | `onInsert` | `onUpdate` | `onDelete` +> & + Pick< + ElectricCollectionConfig, T>, + `onInsert` | `onUpdate` | `onDelete` + > & { + id?: string + utils: ElectricCollectionUtils> + schema: T + } // Overload for when no schema is provided export function electricCollectionOptions>( config: ElectricCollectionConfig & { schema?: never // prohibit schema }, -): Omit, `utils` | `onInsert` | `onUpdate` | `onDelete`> & +): Omit< + CollectionConfig, + `utils` | `onInsert` | `onUpdate` | `onDelete` +> & Pick, `onInsert` | `onUpdate` | `onDelete`> & { - id?: string - utils: ElectricCollectionUtils - schema?: never // no schema in the result -} + id?: string + utils: ElectricCollectionUtils + schema?: never // no schema in the result + } export function electricCollectionOptions>( config: ElectricCollectionConfig, diff --git a/packages/electric-db-collection/tests/electric.test-d.ts b/packages/electric-db-collection/tests/electric.test-d.ts index d951546a9..5075c63f5 100644 --- a/packages/electric-db-collection/tests/electric.test-d.ts +++ b/packages/electric-db-collection/tests/electric.test-d.ts @@ -234,15 +234,33 @@ describe(`Electric collection type resolution tests`, () => { // Verify that the handlers are properly typed with ElectricCollectionUtils expectTypeOf(options.onInsert).parameters.toEqualTypeOf< - [InsertMutationFnParams>] + [ + InsertMutationFnParams< + ExplicitType, + string | number, + ElectricCollectionUtils + >, + ] >() expectTypeOf(options.onUpdate).parameters.toEqualTypeOf< - [UpdateMutationFnParams>] + [ + UpdateMutationFnParams< + ExplicitType, + string | number, + ElectricCollectionUtils + >, + ] >() expectTypeOf(options.onDelete).parameters.toEqualTypeOf< - [DeleteMutationFnParams>] + [ + DeleteMutationFnParams< + ExplicitType, + string | number, + ElectricCollectionUtils + >, + ] >() }) @@ -318,13 +336,31 @@ describe(`Electric collection type resolution tests`, () => { // Verify that the handlers are properly typed with ElectricCollectionUtils expectTypeOf(options.onDelete).parameters.toEqualTypeOf< - [DeleteMutationFnParams>] + [ + DeleteMutationFnParams< + TodoType, + string | number, + ElectricCollectionUtils + >, + ] >() expectTypeOf(options.onInsert).parameters.toEqualTypeOf< - [InsertMutationFnParams>] + [ + InsertMutationFnParams< + TodoType, + string | number, + ElectricCollectionUtils + >, + ] >() expectTypeOf(options.onUpdate).parameters.toEqualTypeOf< - [UpdateMutationFnParams>] + [ + UpdateMutationFnParams< + TodoType, + string | number, + ElectricCollectionUtils + >, + ] >() }) diff --git a/packages/electric-db-collection/tests/electric.test.ts b/packages/electric-db-collection/tests/electric.test.ts index ce6af8b55..f48d2905e 100644 --- a/packages/electric-db-collection/tests/electric.test.ts +++ b/packages/electric-db-collection/tests/electric.test.ts @@ -528,7 +528,11 @@ describe(`Electric Integration`, () => { id: `test-transaction`, mutations: [], } as unknown as TransactionWithMutations - const mockParams: InsertMutationFnParams> = { + const mockParams: InsertMutationFnParams< + Row, + string | number, + ElectricCollectionUtils + > = { transaction: mockTransaction, // @ts-expect-error not relevant to test collection: CollectionImpl, From 4e702a18a2c8e7f74afa101949960684657037bc Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Feb 2026 17:09:13 +0100 Subject: [PATCH 14/14] Add electric-db-collection to changeset Co-Authored-By: Claude Opus 4.6 --- .changeset/tough-dragons-fold.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/tough-dragons-fold.md b/.changeset/tough-dragons-fold.md index cac52eab6..8a93ddc1b 100644 --- a/.changeset/tough-dragons-fold.md +++ b/.changeset/tough-dragons-fold.md @@ -1,7 +1,8 @@ --- '@tanstack/powersync-db-collection': patch '@tanstack/trailbase-db-collection': patch +'@tanstack/electric-db-collection': patch '@tanstack/db': patch --- -Make type of collection utils more precise for localOnly, PowerSync, and Trailbase collections +Make type of collection utils more precise for localOnly, PowerSync, Trailbase, and Electric collections