From 4be6abaf540197baf71f3f2609074f5df740c0e2 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 19 Nov 2025 14:07:55 +0100 Subject: [PATCH 1/3] Add type test reproducing the problem --- .../tests/electric.test-d.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/packages/electric-db-collection/tests/electric.test-d.ts b/packages/electric-db-collection/tests/electric.test-d.ts index 27f90918d..67226bc4d 100644 --- a/packages/electric-db-collection/tests/electric.test-d.ts +++ b/packages/electric-db-collection/tests/electric.test-d.ts @@ -141,6 +141,88 @@ describe(`Electric collection type resolution tests`, () => { >() }) + it(`should correctly type mutations in transaction handlers when mapping over mutations array`, () => { + const schema = z.object({ + id: z.string(), + title: z.string(), + completed: z.boolean(), + }) + + type TodoType = z.infer + + const options = electricCollectionOptions({ + id: `todos`, + schema, + getKey: (item) => item.id, + shapeOptions: { + url: `/api/todos`, + params: { table: `todos` }, + }, + onDelete: (params) => { + // Direct index access should be correctly typed + expectTypeOf( + params.transaction.mutations[0].original + ).toEqualTypeOf() + + // Non-null assertion on second element should be correctly typed + expectTypeOf( + params.transaction.mutations[1]!.original + ).toEqualTypeOf() + + // When mapping over mutations, each mutation.original should be correctly typed + params.transaction.mutations.map((mutation) => { + expectTypeOf(mutation.original).toEqualTypeOf() + return mutation.original.id + }) + + return Promise.resolve({ txid: 1 }) + }, + onInsert: (params) => { + // Direct index access should be correctly typed + expectTypeOf( + params.transaction.mutations[0].modified + ).toEqualTypeOf() + + // When mapping over mutations, each mutation.modified should be correctly typed + params.transaction.mutations.map((mutation) => { + expectTypeOf(mutation.modified).toEqualTypeOf() + return mutation.modified.id + }) + + return Promise.resolve({ txid: 1 }) + }, + onUpdate: (params) => { + // Direct index access should be correctly typed + expectTypeOf( + params.transaction.mutations[0].original + ).toEqualTypeOf() + expectTypeOf( + params.transaction.mutations[0].modified + ).toEqualTypeOf() + + // When mapping over mutations, each mutation should be correctly typed + params.transaction.mutations.map((mutation) => { + expectTypeOf(mutation.original).toEqualTypeOf() + expectTypeOf(mutation.modified).toEqualTypeOf() + return mutation.modified.id + }) + + return Promise.resolve({ txid: 1 }) + }, + }) + + // Verify that the handlers are properly typed + expectTypeOf(options.onDelete).parameters.toEqualTypeOf< + [DeleteMutationFnParams] + >() + expectTypeOf(options.onInsert).parameters.toEqualTypeOf< + [InsertMutationFnParams] + >() + expectTypeOf(options.onUpdate).parameters.toEqualTypeOf< + [UpdateMutationFnParams] + >() + }) + it(`should infer types from Zod schema through electric collection options to live query`, () => { // Define a Zod schema for a user with basic field types const userSchema = z.object({ From 37f374211f17b93a91f867c1b5201509f3ec899d Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 19 Nov 2025 14:37:46 +0100 Subject: [PATCH 2/3] Fix type problem --- packages/db/src/types.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts index d5e82c41f..73944bf4e 100644 --- a/packages/db/src/types.ts +++ b/packages/db/src/types.ts @@ -139,7 +139,26 @@ export type NonEmptyArray = [T, ...Array] export type TransactionWithMutations< T extends object = Record, TOperation extends OperationType = OperationType, -> = Transaction & { +> = Omit, `mutations`> & { + /** + * We must omit the `mutations` property from `Transaction` before intersecting + * because TypeScript intersects property types when the same property appears on + * both sides of an intersection. + * + * Without `Omit`: + * - `Transaction` has `mutations: Array>` + * - The intersection would create: `Array> & NonEmptyArray>` + * - When mapping over this array, TypeScript widens `TOperation` from the specific literal + * (e.g., `"delete"`) to the union `OperationType` (`"insert" | "update" | "delete"`) + * - This causes `PendingMutation` to evaluate the conditional type + * `original: TOperation extends 'insert' ? {} : T` as `{} | T` instead of just `T` + * + * With `Omit`: + * - We remove `mutations` from `Transaction` first + * - Then add back `mutations: NonEmptyArray>` + * - TypeScript can properly narrow `TOperation` to the specific literal type + * - This ensures `mutation.original` is correctly typed as `T` (not `{} | T`) when mapping + */ mutations: NonEmptyArray> } From c1221cce94112b91b56e4fc01abfbe72871a3111 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 19 Nov 2025 14:42:04 +0100 Subject: [PATCH 3/3] Changeset --- .changeset/twelve-pans-act.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/twelve-pans-act.md diff --git a/.changeset/twelve-pans-act.md b/.changeset/twelve-pans-act.md new file mode 100644 index 000000000..a8d578a3f --- /dev/null +++ b/.changeset/twelve-pans-act.md @@ -0,0 +1,6 @@ +--- +"@tanstack/electric-db-collection": patch +"@tanstack/db": patch +--- + +Improve type of mutations in transactions