Skip to content

leftJoin: whole-object Ref extraction loses nullable type (ResultTypeFromSelect missing IsNullableRef check) #1467

@rena0157

Description

@rena0157
  • I've validated the bug against the latest version of DB packages

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions