You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #1678 fixes the first layer of #1677: callbacks over Collection<T> regain keys guaranteed by an unresolved generic constraint such as T extends { id: string }.
Red/green verification uncovered two additional layers where the same constraint is still lost, plus a compatibility edge case in the straightforward nullable-ref fix.
The desired invariant is:
If T extends { id: string }, every query-builder callback should retain access to id, regardless of joins or unions, while preserving existing concrete and exact-nullish ref semantics.
Reproductions
Tested with TypeScript 5.9.3 under the @tanstack/db Vitest typechecker.
1. Nullable generic aliases after leftJoin
functionleftJoinGeneric<Textends{id: string}>(a: Collection<{id: string},string>,b: Collection<T,string>,id: string,){returncreateLiveQueryCollection((q)=>q.from({ a }).leftJoin({ b },({ a, b })=>eq(a.id,b.id)).where(({ b })=>eq(b.id,id)).select(({ b })=>({id: b.id})),)}
Both post-join b.id accesses fail. The ref type contains a propertyless RefLeaf<undefined, true> branch.
2. Source-form unionAll
functionunionSourceGeneric<Textends{id: string}>(a: Collection<T,string>,b: Collection<T,string>,){returncreateLiveQueryCollection((q)=>q.unionAll({ a, b }).where(({ a, b })=>eq(a.id,b.id)),)}
Both a.id and b.id fail for the same nullable-ref reason: source unions add | undefined to each alias.
3. Branch-form unionAll
functionunionGeneric<Textends{id: string}>(a: Collection<T,string>,b: Collection<T,string>,id: string,){returncreateLiveQueryCollection((q)=>{constaRows=q.from({ a })constbRows=q.from({ b })returnq.unionAll(aRows,bRows).where(({id: idRef})=>eq(idRef,id))})}
The where callback has no id. ResultFromBranch uses GetResult, and UnionBranchSchema then performs another mapped reconstruction. The generic constraint is gone before callback refs are built.
Confirmed partial fix and compatibility edge case
For cases 1 and 2, restructuring RefsForContext to strip null | undefined before RefForContextValue distributes fixes the generic failures:
This correctly represents the build-time proxy as present and propagates result nullability through the ref's Nullable flag.
However, unconditional NonNullable<T> changes exact null and exact undefined schema values from RefLeaf<null> / RefLeaf<undefined> to never. A focused type test confirms this regression.
Straightforward exact-nullish guards were also tested. They make the exact-nullish test green but leave the conditional unresolved for generic T | undefined, reintroducing propertyless branches and making cases 1 and 2 red again. A separate NullishKind<T> classifier has the same TypeScript deferral behavior.
Branch-union approaches tested
The following approaches did not provide a safe fix:
Change branch extraction from GetResult to GetRawResult.
The subsequent UnionBranchSchema mapped reconstruction still loses generic keys.
Use the raw branch union directly as refsSchema.
The generic key still does not reliably survive structural inference.
Existing heterogeneous branch behavior regresses.
Intersect or union normalized UnionBranchSchema with raw branch results.
Generic id remains unavailable, or the raw union introduces optional/propertyless branches.
Carry separate raw callback refs alongside normalized refs.
TypeScript reduces the structurally inferred raw generic branch to {} | Ref<T> at the callback boundary, so common properties remain inaccessible.
Propagating raw refs through joins breaks existing typed inner/right/full join tests and subquery compatibility.
Add a phantom/nominal context property to QueryBuilder<TContext>.
Naively adding it changes assignability against QueryBuilder<any> throughout the API and produces widespread overload/variance failures.
Existing tests that must remain green include heterogeneous branch results and typed inner, right, and full joins in union-all.test-d.ts.
Likely design direction
The branch-form problem probably needs the query context/result type to be carried through QueryBuilder in a way that can be extracted nominally or covariantly, rather than recovered from its large structural method surface after GetResult/mapped reconstruction.
A robust design likely needs:
a raw, constraint-preserving branch result used only for immediate callback refs;
the existing normalized branch schema for consumer-facing results and heterogeneous optional keys;
an explicit transition back to normalized refs when later joins reshape the context; and
a QueryBuilder context carrier designed for assignability with QueryBuilder<any>, rather than a simple invariant phantom property.
Separately, nullable-ref construction needs an exact-nullish-safe formulation that TypeScript can resolve for unresolved T | null | undefined without leaving a propertyless union branch.
Summary
PR #1678 fixes the first layer of #1677: callbacks over
Collection<T>regain keys guaranteed by an unresolved generic constraint such asT extends { id: string }.Red/green verification uncovered two additional layers where the same constraint is still lost, plus a compatibility edge case in the straightforward nullable-ref fix.
The desired invariant is:
Reproductions
Tested with TypeScript 5.9.3 under the
@tanstack/dbVitest typechecker.1. Nullable generic aliases after
leftJoinBoth post-join
b.idaccesses fail. The ref type contains a propertylessRefLeaf<undefined, true>branch.2. Source-form
unionAllBoth
a.idandb.idfail for the same nullable-ref reason: source unions add| undefinedto each alias.3. Branch-form
unionAllThe
wherecallback has noid.ResultFromBranchusesGetResult, andUnionBranchSchemathen performs another mapped reconstruction. The generic constraint is gone before callback refs are built.Confirmed partial fix and compatibility edge case
For cases 1 and 2, restructuring
RefsForContextto stripnull | undefinedbeforeRefForContextValuedistributes fixes the generic failures:This correctly represents the build-time proxy as present and propagates result nullability through the ref's
Nullableflag.However, unconditional
NonNullable<T>changes exactnulland exactundefinedschema values fromRefLeaf<null>/RefLeaf<undefined>tonever. A focused type test confirms this regression.Straightforward exact-nullish guards were also tested. They make the exact-nullish test green but leave the conditional unresolved for generic
T | undefined, reintroducing propertyless branches and making cases 1 and 2 red again. A separateNullishKind<T>classifier has the same TypeScript deferral behavior.Branch-union approaches tested
The following approaches did not provide a safe fix:
Change branch extraction from
GetResulttoGetRawResult.UnionBranchSchemamapped reconstruction still loses generic keys.Use the raw branch union directly as
refsSchema.Intersect or union normalized
UnionBranchSchemawith raw branch results.idremains unavailable, or the raw union introduces optional/propertyless branches.Carry separate raw callback refs alongside normalized refs.
{} | Ref<T>at the callback boundary, so common properties remain inaccessible.Add a phantom/nominal context property to
QueryBuilder<TContext>.QueryBuilder<any>throughout the API and produces widespread overload/variance failures.Existing tests that must remain green include heterogeneous branch results and typed inner, right, and full joins in
union-all.test-d.ts.Likely design direction
The branch-form problem probably needs the query context/result type to be carried through
QueryBuilderin a way that can be extracted nominally or covariantly, rather than recovered from its large structural method surface afterGetResult/mapped reconstruction.A robust design likely needs:
QueryBuildercontext carrier designed for assignability withQueryBuilder<any>, rather than a simple invariant phantom property.Separately, nullable-ref construction needs an exact-nullish-safe formulation that TypeScript can resolve for unresolved
T | null | undefinedwithout leaving a propertyless union branch.Acceptance criteria
where,select, and subquery-source cases from Since 0.6.6, queries don't typecheck when the collection's row type is a generic type parameter #1677 remain green.unionAll.unionAll.nulland exactundefinedcontinue producingRefLeaf<null>andRefLeaf<undefined>, notnever.unionAlltests remain green, including typed inner/right/full joins and joined subquery branches.Related: #1677, #1678.