Skip to content

Temporal types lose type information in query select projections #1372

@goatrenterguy

Description

@goatrenterguy

Bug Description

When a schema field is typed as Temporal.PlainDate (or any other Temporal type), using it in a .select() projection causes TypeScript to mangle the type — all methods collapse to {}.

Reproduction

import { Temporal } from 'temporal-polyfill'

type Todo = {
  id: number
  name: string
  dueDate: Temporal.PlainDate
  project_id: number
}

type Project = {
  id: number
  name: string
}

const todos = createCollection<Todo>(/* ... */)
const projects = createCollection<Project>(/* ... */)

const liveQuery = createLiveQueryCollection({
  query: (q) =>
    q
      .from({ todo: todos })
      .innerJoin({ project: projects }, ({ todo, project }) =>
        eq(todo.project_id, project.id),
      )
      .select(({ todo, project }) => ({
        todo,
        project,
      })),
})

// Type error: dueDate is mangled
const date: Temporal.PlainDate = liveQuery.toArray[0]!.todo.dueDate

Error:

Type '{ readonly [Symbol.toStringTag]: "Temporal.PlainDate"; toString: {}; equals: {};
  add: {}; subtract: {}; ... }' is not assignable to type 'PlainDate'.
  Types of property 'equals' are incompatible.
    Type '{}' is not assignable to type '(other: string | PlainDate | PlainDateLike) => boolean'.

Root Cause

In packages/db/src/query/builder/types.ts, IsPlainObject<T> determines how each field is processed through the Ref<T> mapped type:

  • Plain objects → recursively walked to build nested refs
  • Non-plain objects (arrays, JsBuiltIns like Date, Map, etc.) → treated as leaf values, type preserved as-is

Temporal.PlainDate extends object but is not in the JsBuiltIns union, so IsPlainObject returns true. The type system then recursively walks its properties through Ref<T>, which turns all method signatures into {}.

This affects all Temporal types: PlainDate, PlainTime, PlainDateTime, ZonedDateTime, Instant, Duration, etc.

Suggested Fix

Add a Symbol.toStringTag check to IsPlainObject:

type IsPlainObject<T> = T extends unknown
  ? T extends object
    ? T extends ReadonlyArray<any>
      ? false
      : T extends JsBuiltIns
        ? false
        : T extends { [Symbol.toStringTag]: string }
          ? false
          : true
    : false
  : false

Objects with Symbol.toStringTag are class instances (Temporal types, typed arrays, etc.), not plain data objects. This covers all Temporal types without requiring imports or maintaining a hardcoded list.

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