Describe the bug
leftJoin results lose their nullable type when selected as whole objects. Individual property access via RefLeaf correctly produces T | undefined, but selecting the entire joined object via Ref<T, true> produces T instead of T | undefined.
This was partially fixed in #1262 for RefLeaf, but the Ref (whole-object) branch in ResultTypeFromSelect was missed.
To Reproduce
const result = useLiveSuspenseQuery(
(q) =>
q
.from({ order: ordersCollection })
.leftJoin({ dept: departmentsCollection }, ({ order, dept }) =>
eq(order.deptId, dept.id),
)
.select(({ order, dept }) => ({
order,
dept, // typed as Department, should be Department | undefined
})),
[],
)
// TypeScript says dept is non-nullable, but at runtime it IS undefined
// when no matching department exists
result.data[0].dept.name // no type error, crashes at runtime
result.data[0].dept?.name // linter warns "unnecessary optional chain" — but it's required
Expected behavior
Whole-object refs from leftJoin should be typed as T | undefined, matching the existing RefLeaf behavior introduced in #1262.
Root cause
In src/query/builder/types.ts, ResultTypeFromSelect handles Ref and RefLeaf differently:
// Ref branch (whole object) — MISSING IsNullableRef check
TSelectObject[K] extends Ref<infer _T>
? ExtractRef<TSelectObject[K]>
// RefLeaf branch (individual property) — correctly checks IsNullableRef
TSelectObject[K] extends RefLeaf<infer T>
? IsNullableRef<TSelectObject[K]> extends true
? T | undefined
: T
ExtractRef calls WithoutRefBrand<T> which strips NullableBrand, so the Nullable = true flag from Ref<T, true> is lost before it can be checked.
Proposed fix
Add the IsNullableRef check to the Ref branch, matching the existing RefLeaf pattern:
- TSelectObject[K] extends Ref<infer _T>
- ? ExtractRef<TSelectObject[K]>
+ TSelectObject[K] extends Ref<infer _T>
+ ? IsNullableRef<TSelectObject[K]> extends true
+ ? ExtractRef<TSelectObject[K]> | undefined
+ : ExtractRef<TSelectObject[K]>
Desktop
- OS: macOS
@tanstack/db: 0.6.4
- TypeScript: 6.0
Additional context
Describe the bug
leftJoinresults lose their nullable type when selected as whole objects. Individual property access viaRefLeafcorrectly producesT | undefined, but selecting the entire joined object viaRef<T, true>producesTinstead ofT | undefined.This was partially fixed in #1262 for
RefLeaf, but theRef(whole-object) branch inResultTypeFromSelectwas missed.To Reproduce
Expected behavior
Whole-object refs from
leftJoinshould be typed asT | undefined, matching the existingRefLeafbehavior introduced in #1262.Root cause
In
src/query/builder/types.ts,ResultTypeFromSelecthandlesRefandRefLeafdifferently:ExtractRefcallsWithoutRefBrand<T>which stripsNullableBrand, so theNullable = trueflag fromRef<T, true>is lost before it can be checked.Proposed fix
Add the
IsNullableRefcheck to theRefbranch, matching the existingRefLeafpattern:Desktop
@tanstack/db: 0.6.4Additional context
Ref<T, Nullable>brand pattern@tanstack/db@0.6.4