From ece573730d019a3278b4b793b5ca568a6a39ea2c Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sun, 20 Sep 2026 10:28:19 +0100 Subject: [PATCH 1/6] fix(frameworks): align live query result types --- .../fix-framework-live-query-result-types.md | 9 +++ .../tests/inject-live-query.test-d.ts | 24 ++++++++ packages/react-db/src/useLiveInfiniteQuery.ts | 26 ++++++++- .../tests/useLiveInfiniteQuery.test-d.tsx | 26 ++++++++- .../react-db/tests/useLiveQuery.test-d.tsx | 26 +++++++++ .../solid-db/tests/useLiveQuery.test-d.tsx | 26 +++++++++ packages/svelte-db/src/useLiveQuery.svelte.ts | 13 +++-- .../svelte-db/tests/useLiveQuery.test-d.ts | 56 +++++++++++++++++++ packages/vue-db/src/useLiveQuery.ts | 14 ++++- packages/vue-db/tests/useLiveQuery.test-d.ts | 31 ++++++++++ 10 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-framework-live-query-result-types.md create mode 100644 packages/svelte-db/tests/useLiveQuery.test-d.ts diff --git a/.changeset/fix-framework-live-query-result-types.md b/.changeset/fix-framework-live-query-result-types.md new file mode 100644 index 0000000000..bf61adecdf --- /dev/null +++ b/.changeset/fix-framework-live-query-result-types.md @@ -0,0 +1,9 @@ +--- +'@tanstack/react-db': patch +'@tanstack/svelte-db': patch +'@tanstack/vue-db': patch +--- + +Preserve pre-created collection row, key, and utility types in React infinite +queries, and include the disabled collection and status states in Vue and +Svelte conditional live-query callback results. diff --git a/packages/angular-db/tests/inject-live-query.test-d.ts b/packages/angular-db/tests/inject-live-query.test-d.ts index d83f88ba76..cc5ffb67a5 100644 --- a/packages/angular-db/tests/inject-live-query.test-d.ts +++ b/packages/angular-db/tests/inject-live-query.test-d.ts @@ -7,6 +7,7 @@ import { liveQueryCollectionOptions, } from '../../db/src/query/index' import { injectLiveQuery } from '../src/index' +import type { CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' @@ -133,4 +134,27 @@ describe(`injectLiveQuery type assertions`, () => { Array> >() }) + + it(`types disabled callbacks from their empty reactive runtime`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-angular`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + const result = injectLiveQuery((q) => + enabled ? q.from({ collection }) : undefined, + ) + + const data: Array> = result.data() + expectTypeOf(data).toEqualTypeOf>>() + expectTypeOf().toExtend>() + expectTypeOf(result.status()).toEqualTypeOf() + + // @ts-expect-error Disabled callbacks expose a null collection until enabled. + result.collection().preload() + }) }) diff --git a/packages/react-db/src/useLiveInfiniteQuery.ts b/packages/react-db/src/useLiveInfiniteQuery.ts index a9a657a669..8711f45dea 100644 --- a/packages/react-db/src/useLiveInfiniteQuery.ts +++ b/packages/react-db/src/useLiveInfiniteQuery.ts @@ -28,6 +28,7 @@ import type { import type { Collection, CollectionImpl as CollectionImplType, + CollectionStatus, Context, DbClient, InferResultType, @@ -69,6 +70,29 @@ export type UseLiveInfiniteQueryReturn = Omit< error: unknown } +export type UseLiveInfiniteQueryReturnWithCollection< + TResult extends object, + TKey extends string | number, + TUtils extends Record, +> = { + data: Array + state: Map + collection: Collection & NonSingleResult + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean + isEnabled: true + pages: Array> + pageParams: Array + fetchNextPage: () => Promise + hasNextPage: boolean + isFetchingNextPage: boolean + error: unknown +} + type EnabledLiveQueryReturn = ReturnType< typeof useLiveQuery > @@ -111,7 +135,7 @@ export function useLiveInfiniteQuery< >( liveQueryCollection: Collection & NonSingleResult, config: UseLiveInfiniteQueryConfig, -): UseLiveInfiniteQueryReturn +): UseLiveInfiniteQueryReturnWithCollection // Overload for query function export function useLiveInfiniteQuery( diff --git a/packages/react-db/tests/useLiveInfiniteQuery.test-d.tsx b/packages/react-db/tests/useLiveInfiniteQuery.test-d.tsx index dd3923af7a..cd9aa4db8d 100644 --- a/packages/react-db/tests/useLiveInfiniteQuery.test-d.tsx +++ b/packages/react-db/tests/useLiveInfiniteQuery.test-d.tsx @@ -1,9 +1,10 @@ import { describe, expectTypeOf, it } from 'vitest' +import { useLiveInfiniteQuery } from '../src/useLiveInfiniteQuery' +import type { Collection, Context, NonSingleResult } from '@tanstack/db' import type { UseLiveInfiniteQueryConfig, UseLiveInfiniteQueryReturn, } from '../src/useLiveInfiniteQuery' -import type { Context } from '@tanstack/db' describe(`useLiveInfiniteQuery type assertions`, () => { it(`does not advertise a server-page callback`, () => { @@ -26,4 +27,27 @@ describe(`useLiveInfiniteQuery type assertions`, () => { UseLiveInfiniteQueryReturn[`fetchNextPage`] >().toEqualTypeOf<() => Promise>() }) + + it(`preserves pre-created collection row, key, and utility types`, () => { + type Post = { id: `post-${number}`; title: string } + type PostKey = Post[`id`] + type PostUtils = { refreshPost: (key: PostKey) => Promise } + + const collection = null as unknown as Collection & + NonSingleResult + const result = useLiveInfiniteQuery(collection, { pageSize: 5 }) + + expectTypeOf(result.data).toEqualTypeOf>() + expectTypeOf(result.pages).toEqualTypeOf>>() + expectTypeOf(result.state).toEqualTypeOf>() + expectTypeOf(result.collection).toEqualTypeOf() + expectTypeOf(result.collection.utils.refreshPost).toEqualTypeOf< + (key: PostKey) => Promise + >() + + // @ts-expect-error The collection overload must not erase row fields to any. + result.data[0]!.missing + // @ts-expect-error The collection overload must preserve the collection key. + result.state.get(`not-a-post-key`) + }) }) diff --git a/packages/react-db/tests/useLiveQuery.test-d.tsx b/packages/react-db/tests/useLiveQuery.test-d.tsx index c541e71e74..bcdc570700 100644 --- a/packages/react-db/tests/useLiveQuery.test-d.tsx +++ b/packages/react-db/tests/useLiveQuery.test-d.tsx @@ -178,6 +178,32 @@ describe(`useLiveQuery type assertions`, () => { expectTypeOf(result.current.isEnabled).toEqualTypeOf() }) + it(`types disabled callbacks with React's absent result representation`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-callback`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + const { result } = renderHook(() => + useLiveQuery((q) => (enabled ? q.from({ collection }) : null)), + ) + + const data: Array> | undefined = + result.current.data + expectTypeOf(data).toEqualTypeOf< + Array> | undefined + >() + expectTypeOf(result.current.status).toEqualTypeOf() + expectTypeOf(result.current.isEnabled).toEqualTypeOf() + + // @ts-expect-error React omits disabled query data until the callback enables it. + result.current.data.map((person) => person.id) + }) + it(`rejects a conditional config with a top-level scalar result`, () => { const collection = createCollection( mockSyncCollectionOptions({ diff --git a/packages/solid-db/tests/useLiveQuery.test-d.tsx b/packages/solid-db/tests/useLiveQuery.test-d.tsx index 29c5061ea7..e08786cb1b 100644 --- a/packages/solid-db/tests/useLiveQuery.test-d.tsx +++ b/packages/solid-db/tests/useLiveQuery.test-d.tsx @@ -4,6 +4,7 @@ import { createCollection } from '../../db/src/collection/index' import { mockSyncCollectionOptions } from '../../db/tests/utils' import { createLiveQueryCollection, eq } from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery' +import type { CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' @@ -85,4 +86,29 @@ describe(`useLiveQuery type assertions`, () => { Array> >() }) + + it(`types disabled callbacks from their empty reactive runtime`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-solid`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + const rendered = renderHook(() => + useLiveQuery((q) => (enabled ? q.from({ collection }) : null)), + ) + + const data: Array> = rendered.result() + expectTypeOf(data).toEqualTypeOf>>() + expectTypeOf().toExtend() + expectTypeOf(rendered.result.status).toEqualTypeOf< + CollectionStatus | `disabled` + >() + + // @ts-expect-error Disabled callbacks expose a null collection until enabled. + rendered.result.collection.preload() + }) }) diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index 5af53f6b55..7d89f33ef9 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -56,6 +56,14 @@ export interface UseLiveQueryReturn> { isCleanedUp: boolean } +type ConditionalUseLiveQueryReturn> = Omit< + UseLiveQueryReturn, + `collection` | `status` +> & { + collection: Collection | null + status: CollectionStatus | `disabled` +} + export interface UseLiveQueryReturnWithCollection< T extends object, TKey extends string | number, @@ -188,10 +196,7 @@ export function useLiveQuery( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, deps?: Array<() => unknown>, -): UseLiveQueryReturn< - GetResult, - InferResultType | undefined -> +): ConditionalUseLiveQueryReturn, InferResultType> /** * Create a live query using configuration object diff --git a/packages/svelte-db/tests/useLiveQuery.test-d.ts b/packages/svelte-db/tests/useLiveQuery.test-d.ts new file mode 100644 index 0000000000..92c01ec473 --- /dev/null +++ b/packages/svelte-db/tests/useLiveQuery.test-d.ts @@ -0,0 +1,56 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { createCollection } from '../../db/src/collection/index' +import { mockSyncCollectionOptions } from '../../db/tests/utils' +import { useLiveQuery } from '../src/useLiveQuery.svelte.js' +import type { Collection, CollectionStatus } from '@tanstack/db' +import type { OutputWithVirtual } from '../../db/tests/utils' + +type Person = { + id: string + name: string +} + +describe(`useLiveQuery type assertions`, () => { + it(`types disabled-capable callbacks from their empty reactive runtime`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-svelte`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + const result = useLiveQuery((q) => + enabled ? q.from({ collection }) : undefined, + ) + + const data: Array> = result.data + expectTypeOf(data).toEqualTypeOf>>() + expectTypeOf().toExtend() + expectTypeOf(result.collection).toMatchTypeOf, + string | number, + {} + > | null>() + expectTypeOf(result.status).toEqualTypeOf() + + // @ts-expect-error Disabled callbacks expose a null collection until enabled. + result.collection.preload() + }) + + it(`preserves findOne cardinality`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-find-one-svelte`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + + const result = useLiveQuery((q) => q.from({ collection }).findOne()) + + const data: OutputWithVirtual | undefined = result.data + expectTypeOf(data).toEqualTypeOf | undefined>() + }) +}) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 40a2fafb9d..2dc3e40ce1 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -53,6 +53,18 @@ export interface UseLiveQueryReturn { isCleanedUp: ComputedRef } +type ConditionalUseLiveQueryReturn = Omit< + UseLiveQueryReturn, + `collection` | `status` +> & { + collection: ComputedRef, + string | number, + {} + > | null> + status: ComputedRef +} + export interface UseLiveQueryReturnWithCollection< T extends object, TKey extends string | number, @@ -146,7 +158,7 @@ export function useLiveQuery( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, deps?: Array>, -): UseLiveQueryReturn +): ConditionalUseLiveQueryReturn /** * Create a live query using configuration object diff --git a/packages/vue-db/tests/useLiveQuery.test-d.ts b/packages/vue-db/tests/useLiveQuery.test-d.ts index 8c1d5077ed..441e23c73e 100644 --- a/packages/vue-db/tests/useLiveQuery.test-d.ts +++ b/packages/vue-db/tests/useLiveQuery.test-d.ts @@ -7,6 +7,7 @@ import { liveQueryCollectionOptions, } from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery' +import type { Collection, CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' @@ -145,4 +146,34 @@ describe(`useLiveQuery type assertions`, () => { Array> >() }) + + it(`types disabled-capable callbacks from their empty reactive runtime`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-vue`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + const result = useLiveQuery((q) => + enabled ? q.from({ collection }) : null, + ) + + const data: Array> = result.data.value + expectTypeOf(data).toEqualTypeOf>>() + expectTypeOf().toExtend() + expectTypeOf(result.collection.value).toMatchTypeOf, + string | number, + {} + > | null>() + expectTypeOf(result.status.value).toEqualTypeOf< + CollectionStatus | `disabled` + >() + + // @ts-expect-error Disabled callbacks expose a null collection until enabled. + result.collection.value.preload() + }) }) From 4b40532a1e711fcd52e780c3adc77421c7fcc62e Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sun, 20 Sep 2026 13:04:06 +0100 Subject: [PATCH 2/6] test(frameworks): assert complete conditional collection types --- .../angular-db/tests/inject-live-query.test-d.ts | 9 +++++++-- packages/solid-db/tests/useLiveQuery.test-d.tsx | 14 +++++++++++--- packages/svelte-db/tests/useLiveQuery.test-d.ts | 6 +++--- packages/vue-db/tests/useLiveQuery.test-d.ts | 6 +++--- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/angular-db/tests/inject-live-query.test-d.ts b/packages/angular-db/tests/inject-live-query.test-d.ts index cc5ffb67a5..72a4dbcf47 100644 --- a/packages/angular-db/tests/inject-live-query.test-d.ts +++ b/packages/angular-db/tests/inject-live-query.test-d.ts @@ -5,9 +5,10 @@ import { createLiveQueryCollection, eq, liveQueryCollectionOptions, + type Prettify, } from '../../db/src/query/index' import { injectLiveQuery } from '../src/index' -import type { CollectionStatus } from '@tanstack/db' +import type { Collection, CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' @@ -151,7 +152,11 @@ describe(`injectLiveQuery type assertions`, () => { const data: Array> = result.data() expectTypeOf(data).toEqualTypeOf>>() - expectTypeOf().toExtend>() + expectTypeOf(result.collection()).toEqualTypeOf>, + string | number, + {} + > | null>() expectTypeOf(result.status()).toEqualTypeOf() // @ts-expect-error Disabled callbacks expose a null collection until enabled. diff --git a/packages/solid-db/tests/useLiveQuery.test-d.tsx b/packages/solid-db/tests/useLiveQuery.test-d.tsx index e08786cb1b..a54ed5c8dd 100644 --- a/packages/solid-db/tests/useLiveQuery.test-d.tsx +++ b/packages/solid-db/tests/useLiveQuery.test-d.tsx @@ -2,9 +2,13 @@ import { describe, expectTypeOf, it } from 'vitest' import { renderHook } from '@solidjs/testing-library' import { createCollection } from '../../db/src/collection/index' import { mockSyncCollectionOptions } from '../../db/tests/utils' -import { createLiveQueryCollection, eq } from '../../db/src/query/index' +import { + createLiveQueryCollection, + eq, + type Prettify, +} from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery' -import type { CollectionStatus } from '@tanstack/db' +import type { Collection, CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' @@ -103,7 +107,11 @@ describe(`useLiveQuery type assertions`, () => { const data: Array> = rendered.result() expectTypeOf(data).toEqualTypeOf>>() - expectTypeOf().toExtend() + expectTypeOf(rendered.result.collection).toEqualTypeOf>, + string | number, + {} + > | null>() expectTypeOf(rendered.result.status).toEqualTypeOf< CollectionStatus | `disabled` >() diff --git a/packages/svelte-db/tests/useLiveQuery.test-d.ts b/packages/svelte-db/tests/useLiveQuery.test-d.ts index 92c01ec473..baba565ac7 100644 --- a/packages/svelte-db/tests/useLiveQuery.test-d.ts +++ b/packages/svelte-db/tests/useLiveQuery.test-d.ts @@ -1,6 +1,7 @@ import { describe, expectTypeOf, it } from 'vitest' import { createCollection } from '../../db/src/collection/index' import { mockSyncCollectionOptions } from '../../db/tests/utils' +import type { Prettify } from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery.svelte.js' import type { Collection, CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' @@ -27,9 +28,8 @@ describe(`useLiveQuery type assertions`, () => { const data: Array> = result.data expectTypeOf(data).toEqualTypeOf>>() - expectTypeOf().toExtend() - expectTypeOf(result.collection).toMatchTypeOf, + expectTypeOf(result.collection).toEqualTypeOf>, string | number, {} > | null>() diff --git a/packages/vue-db/tests/useLiveQuery.test-d.ts b/packages/vue-db/tests/useLiveQuery.test-d.ts index 441e23c73e..d6ef59060d 100644 --- a/packages/vue-db/tests/useLiveQuery.test-d.ts +++ b/packages/vue-db/tests/useLiveQuery.test-d.ts @@ -5,6 +5,7 @@ import { createLiveQueryCollection, eq, liveQueryCollectionOptions, + type Prettify, } from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery' import type { Collection, CollectionStatus } from '@tanstack/db' @@ -163,9 +164,8 @@ describe(`useLiveQuery type assertions`, () => { const data: Array> = result.data.value expectTypeOf(data).toEqualTypeOf>>() - expectTypeOf().toExtend() - expectTypeOf(result.collection.value).toMatchTypeOf, + expectTypeOf(result.collection.value).toEqualTypeOf>, string | number, {} > | null>() From 661174848ac8fc99419b3eb817724294c04c1a68 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sun, 20 Sep 2026 13:07:59 +0100 Subject: [PATCH 3/6] chore(frameworks): satisfy type-test import lint --- packages/angular-db/tests/inject-live-query.test-d.ts | 2 +- packages/solid-db/tests/useLiveQuery.test-d.tsx | 7 ++----- packages/svelte-db/tests/useLiveQuery.test-d.ts | 2 +- packages/vue-db/tests/useLiveQuery.test-d.ts | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/angular-db/tests/inject-live-query.test-d.ts b/packages/angular-db/tests/inject-live-query.test-d.ts index 72a4dbcf47..486de79c22 100644 --- a/packages/angular-db/tests/inject-live-query.test-d.ts +++ b/packages/angular-db/tests/inject-live-query.test-d.ts @@ -5,9 +5,9 @@ import { createLiveQueryCollection, eq, liveQueryCollectionOptions, - type Prettify, } from '../../db/src/query/index' import { injectLiveQuery } from '../src/index' +import type { Prettify } from '../../db/src/query/index' import type { Collection, CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' diff --git a/packages/solid-db/tests/useLiveQuery.test-d.tsx b/packages/solid-db/tests/useLiveQuery.test-d.tsx index a54ed5c8dd..72537d4372 100644 --- a/packages/solid-db/tests/useLiveQuery.test-d.tsx +++ b/packages/solid-db/tests/useLiveQuery.test-d.tsx @@ -2,12 +2,9 @@ import { describe, expectTypeOf, it } from 'vitest' import { renderHook } from '@solidjs/testing-library' import { createCollection } from '../../db/src/collection/index' import { mockSyncCollectionOptions } from '../../db/tests/utils' -import { - createLiveQueryCollection, - eq, - type Prettify, -} from '../../db/src/query/index' +import { createLiveQueryCollection, eq } from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery' +import type { Prettify } from '../../db/src/query/index' import type { Collection, CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' diff --git a/packages/svelte-db/tests/useLiveQuery.test-d.ts b/packages/svelte-db/tests/useLiveQuery.test-d.ts index baba565ac7..6a57606b9e 100644 --- a/packages/svelte-db/tests/useLiveQuery.test-d.ts +++ b/packages/svelte-db/tests/useLiveQuery.test-d.ts @@ -1,8 +1,8 @@ import { describe, expectTypeOf, it } from 'vitest' import { createCollection } from '../../db/src/collection/index' import { mockSyncCollectionOptions } from '../../db/tests/utils' -import type { Prettify } from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery.svelte.js' +import type { Prettify } from '../../db/src/query/index' import type { Collection, CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' diff --git a/packages/vue-db/tests/useLiveQuery.test-d.ts b/packages/vue-db/tests/useLiveQuery.test-d.ts index d6ef59060d..b2b72a0e1f 100644 --- a/packages/vue-db/tests/useLiveQuery.test-d.ts +++ b/packages/vue-db/tests/useLiveQuery.test-d.ts @@ -5,9 +5,9 @@ import { createLiveQueryCollection, eq, liveQueryCollectionOptions, - type Prettify, } from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery' +import type { Prettify } from '../../db/src/query/index' import type { Collection, CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' From 22af5725718f3c4c24c7cb7f3513ba4087151f4f Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sun, 20 Sep 2026 16:36:05 +0100 Subject: [PATCH 4/6] fix(frameworks): type disabled single-result data --- .../fix-framework-live-query-result-types.md | 7 +++- packages/angular-db/src/index.ts | 16 +++++++- .../tests/inject-live-query.test-d.ts | 19 ++++++++++ .../tests/inject-live-query.test.ts | 36 ++++++++++++++++++ packages/solid-db/src/useLiveQuery.ts | 9 ++++- .../solid-db/tests/useLiveQuery.test-d.tsx | 21 ++++++++++ packages/solid-db/tests/useLiveQuery.test.tsx | 38 +++++++++++++++++++ packages/svelte-db/src/useLiveQuery.svelte.ts | 18 ++++++--- .../tests/useLiveQuery.svelte.test.ts | 37 ++++++++++++++++++ .../svelte-db/tests/useLiveQuery.test-d.ts | 25 ++++++++++++ packages/vue-db/src/useLiveQuery.ts | 10 ++++- packages/vue-db/tests/useLiveQuery.test-d.ts | 32 +++++++++++++++- packages/vue-db/tests/useLiveQuery.test.ts | 35 +++++++++++++++++ 13 files changed, 289 insertions(+), 14 deletions(-) diff --git a/.changeset/fix-framework-live-query-result-types.md b/.changeset/fix-framework-live-query-result-types.md index bf61adecdf..95ccaac5a2 100644 --- a/.changeset/fix-framework-live-query-result-types.md +++ b/.changeset/fix-framework-live-query-result-types.md @@ -1,9 +1,12 @@ --- +'@tanstack/angular-db': patch '@tanstack/react-db': patch +'@tanstack/solid-db': patch '@tanstack/svelte-db': patch '@tanstack/vue-db': patch --- Preserve pre-created collection row, key, and utility types in React infinite -queries, and include the disabled collection and status states in Vue and -Svelte conditional live-query callback results. +queries. Align conditional live-query result types with each framework's +disabled representation, including nullable collections, disabled statuses, +and empty single-result data in the empty-reactive bindings. diff --git a/packages/angular-db/src/index.ts b/packages/angular-db/src/index.ts index ba579c31a6..6d4fbad6a6 100644 --- a/packages/angular-db/src/index.ts +++ b/packages/angular-db/src/index.ts @@ -58,6 +58,18 @@ export interface InjectLiveQueryResult { isCleanedUp: Signal } +type InferConditionalResultType = + TContext extends SingleResult + ? InferResultType | [] + : InferResultType + +export type InjectConditionalLiveQueryResult = Omit< + InjectLiveQueryResult, + `data` +> & { + data: Signal> +} + export interface InjectLiveQueryResultWithCollection< TResult extends object = any, TKey extends string | number = string | number, @@ -109,7 +121,7 @@ export function injectLiveQuery< params: TParams q: InitialQueryBuilder }) => QueryBuilder | undefined | null -}): InjectLiveQueryResult +}): InjectConditionalLiveQueryResult export function injectLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, ): InjectLiveQueryResult @@ -117,7 +129,7 @@ export function injectLiveQuery( queryFn: ( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, -): InjectLiveQueryResult +): InjectConditionalLiveQueryResult export function injectLiveQuery( config: LiveQueryCollectionConfig, ): InjectLiveQueryResult diff --git a/packages/angular-db/tests/inject-live-query.test-d.ts b/packages/angular-db/tests/inject-live-query.test-d.ts index 486de79c22..358cc88c40 100644 --- a/packages/angular-db/tests/inject-live-query.test-d.ts +++ b/packages/angular-db/tests/inject-live-query.test-d.ts @@ -162,4 +162,23 @@ describe(`injectLiveQuery type assertions`, () => { // @ts-expect-error Disabled callbacks expose a null collection until enabled. result.collection().preload() }) + + it(`types conditional findOne data with its empty disabled representation`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-find-one-angular`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + const result = injectLiveQuery((q) => + enabled ? q.from({ collection }).findOne() : null, + ) + + expectTypeOf(result.data()).toEqualTypeOf< + Prettify> | undefined | [] + >() + }) }) diff --git a/packages/angular-db/tests/inject-live-query.test.ts b/packages/angular-db/tests/inject-live-query.test.ts index a2dceb0591..1618d32fea 100644 --- a/packages/angular-db/tests/inject-live-query.test.ts +++ b/packages/angular-db/tests/inject-live-query.test.ts @@ -1208,5 +1208,41 @@ describe(`injectLiveQuery`, () => { expect(result.data()).toEqual([]) }) }) + + it(`keeps conditional findOne data empty while disabled`, async () => { + await TestBed.runInInjectionContext(async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-find-one-angular`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + const enabled = signal(false) + const result = injectLiveQuery({ + params: () => ({ enabled: enabled() }), + query: ({ params, q }) => + params.enabled + ? q + .from({ collection }) + .where(({ collection: person }) => eq(person.id, `3`)) + .findOne() + : null, + }) + + await waitForAngularUpdate() + expect(result.status()).toBe(`disabled`) + expect(result.data()).toEqual([]) + + enabled.set(true) + await waitForAngularUpdate() + expect(result.data()).toMatchObject({ id: `3` }) + + enabled.set(false) + await waitForAngularUpdate() + expect(result.status()).toBe(`disabled`) + expect(result.data()).toEqual([]) + }) + }) }) }) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 67e421be7a..5fd3c43116 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -30,6 +30,11 @@ import type { SingleResult, } from '@tanstack/db' +type InferConditionalResultType = + TContext extends SingleResult + ? InferResultType | [] + : InferResultType + /** * Create a live query using a query function * @param queryFn - Query function that defines what data to fetch @@ -123,12 +128,12 @@ export function useLiveQuery( queryFn: ( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, -): Accessor> & { +): Accessor> & { /** * @deprecated use function result instead * query.data -> query() */ - data: InferResultType + data: InferConditionalResultType state: ReactiveMap> collection: Collection, string | number, {}> | null status: CollectionStatus | `disabled` diff --git a/packages/solid-db/tests/useLiveQuery.test-d.tsx b/packages/solid-db/tests/useLiveQuery.test-d.tsx index 72537d4372..2ee88d2112 100644 --- a/packages/solid-db/tests/useLiveQuery.test-d.tsx +++ b/packages/solid-db/tests/useLiveQuery.test-d.tsx @@ -116,4 +116,25 @@ describe(`useLiveQuery type assertions`, () => { // @ts-expect-error Disabled callbacks expose a null collection until enabled. rendered.result.collection.preload() }) + + it(`types conditional findOne data with its empty disabled representation`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-find-one-solid`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + const rendered = renderHook(() => + useLiveQuery((q) => + enabled ? q.from({ collection }).findOne() : undefined, + ), + ) + + expectTypeOf(rendered.result()).toEqualTypeOf< + Prettify> | undefined | [] + >() + }) }) diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index e92572a2ae..3883c1bde9 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -2077,6 +2077,44 @@ describe(`Query Collections`, () => { dispose() }) }) + + it(`keeps conditional findOne data empty while disabled`, async () => { + return createRoot(async (dispose) => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-find-one-solid`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + const [enabled, setEnabled] = createSignal(false) + const rendered = renderHook(() => + useLiveQuery((q) => + enabled() + ? q + .from({ collection }) + .where(({ collection: person }) => eq(person.id, `3`)) + .findOne() + : null, + ), + ) + + expect(rendered.result.status).toBe(`disabled`) + expect(rendered.result()).toEqual([]) + + setEnabled(true) + await waitFor(() => { + expect(rendered.result()).toMatchObject({ id: `3` }) + }) + + setEnabled(false) + await waitFor(() => { + expect(rendered.result.status).toBe(`disabled`) + }) + expect(rendered.result()).toEqual([]) + dispose() + }) + }) }) describe(`Suspense Integration`, () => { diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index 7d89f33ef9..f12df5fab0 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -56,10 +56,15 @@ export interface UseLiveQueryReturn> { isCleanedUp: boolean } -type ConditionalUseLiveQueryReturn> = Omit< - UseLiveQueryReturn, - `collection` | `status` -> & { +type InferConditionalResultType = + TContext extends SingleResult + ? InferResultType | [] + : InferResultType + +export type ConditionalUseLiveQueryReturn< + T extends object, + TData = Array, +> = Omit, `collection` | `status`> & { collection: Collection | null status: CollectionStatus | `disabled` } @@ -196,7 +201,10 @@ export function useLiveQuery( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, deps?: Array<() => unknown>, -): ConditionalUseLiveQueryReturn, InferResultType> +): ConditionalUseLiveQueryReturn< + GetResult, + InferConditionalResultType +> /** * Create a live query using configuration object diff --git a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts index 5d490f3036..a956b03e6b 100644 --- a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts +++ b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts @@ -2186,5 +2186,42 @@ describe(`Query Collections`, () => { expect(query.state.size).toBe(0) }) }) + + it(`keeps conditional findOne data empty while disabled`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-find-one-svelte`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + cleanup = $effect.root(() => { + let enabled = $state(false) + const query = useLiveQuery( + (q) => + enabled + ? q + .from({ collection }) + .where(({ collection: person }) => eq(person.id, `3`)) + .findOne() + : null, + [() => enabled], + ) + + flushSync() + expect(query.status).toBe(`disabled`) + expect(query.data).toEqual([]) + + enabled = true + flushSync() + expect(query.data).toMatchObject({ id: `3` }) + + enabled = false + flushSync() + expect(query.status).toBe(`disabled`) + expect(query.data).toEqual([]) + }) + }) }) }) diff --git a/packages/svelte-db/tests/useLiveQuery.test-d.ts b/packages/svelte-db/tests/useLiveQuery.test-d.ts index 6a57606b9e..51c731be37 100644 --- a/packages/svelte-db/tests/useLiveQuery.test-d.ts +++ b/packages/svelte-db/tests/useLiveQuery.test-d.ts @@ -2,6 +2,7 @@ import { describe, expectTypeOf, it } from 'vitest' import { createCollection } from '../../db/src/collection/index' import { mockSyncCollectionOptions } from '../../db/tests/utils' import { useLiveQuery } from '../src/useLiveQuery.svelte.js' +import type { ConditionalUseLiveQueryReturn } from '../src/index.js' import type { Prettify } from '../../db/src/query/index' import type { Collection, CollectionStatus } from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' @@ -53,4 +54,28 @@ describe(`useLiveQuery type assertions`, () => { const data: OutputWithVirtual | undefined = result.data expectTypeOf(data).toEqualTypeOf | undefined>() }) + + it(`types conditional findOne data with its empty disabled representation`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-find-one-svelte`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + const result = useLiveQuery((q) => + enabled ? q.from({ collection }).findOne() : null, + ) + const annotated: ConditionalUseLiveQueryReturn< + Prettify>, + Prettify> | undefined | [] + > = result + + expectTypeOf(annotated).toEqualTypeOf() + expectTypeOf(result.data).toEqualTypeOf< + Prettify> | undefined | [] + >() + }) }) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 2dc3e40ce1..efd233111b 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -53,10 +53,16 @@ export interface UseLiveQueryReturn { isCleanedUp: ComputedRef } -type ConditionalUseLiveQueryReturn = Omit< +type InferConditionalResultType = + TContext extends SingleResult + ? InferResultType | [] + : InferResultType + +export type ConditionalUseLiveQueryReturn = Omit< UseLiveQueryReturn, - `collection` | `status` + `data` | `collection` | `status` > & { + data: ComputedRef> collection: ComputedRef, string | number, diff --git a/packages/vue-db/tests/useLiveQuery.test-d.ts b/packages/vue-db/tests/useLiveQuery.test-d.ts index b2b72a0e1f..30594c15ef 100644 --- a/packages/vue-db/tests/useLiveQuery.test-d.ts +++ b/packages/vue-db/tests/useLiveQuery.test-d.ts @@ -7,8 +7,14 @@ import { liveQueryCollectionOptions, } from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery' +import type { ConditionalUseLiveQueryReturn } from '../src/index' import type { Prettify } from '../../db/src/query/index' -import type { Collection, CollectionStatus } from '@tanstack/db' +import type { + Collection, + CollectionStatus, + InitialQueryBuilder, + QueryBuilder, +} from '@tanstack/db' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' @@ -176,4 +182,28 @@ describe(`useLiveQuery type assertions`, () => { // @ts-expect-error Disabled callbacks expose a null collection until enabled. result.collection.value.preload() }) + + it(`types conditional findOne data with its empty disabled representation`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-find-one-vue`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + const build = (q: InitialQueryBuilder) => q.from({ collection }).findOne() + type QueryContext = + ReturnType extends QueryBuilder + ? TContext + : never + + const result = useLiveQuery((q) => (enabled ? build(q) : null)) + const annotated: ConditionalUseLiveQueryReturn = result + + expectTypeOf(annotated).toEqualTypeOf() + expectTypeOf(result.data.value).toEqualTypeOf< + Prettify> | undefined | [] + >() + }) }) diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 657aec902f..eb654b955c 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -1933,5 +1933,40 @@ describe(`Query Collections`, () => { expect(result.data.value).toHaveLength(1) expect(result.isReady.value).toBe(true) }) + + it(`keeps conditional findOne data empty while disabled`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-find-one-vue`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + const enabled = ref(false) + const result = useLiveQuery( + (q) => + enabled.value + ? q + .from({ collection }) + .where(({ collection: person }) => eq(person.id, `3`)) + .findOne() + : null, + [() => enabled.value], + ) + + expect(result.status.value).toBe(`disabled`) + expect(result.data.value).toEqual([]) + + enabled.value = true + await waitFor(() => { + expect(result.data.value).toMatchObject({ id: `3` }) + }) + + enabled.value = false + await waitFor(() => { + expect(result.status.value).toBe(`disabled`) + }) + expect(result.data.value).toEqual([]) + }) }) }) From 6cc42019527cd2929ebb0bae0c128e8d22391e5a Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Mon, 21 Sep 2026 09:52:32 +0100 Subject: [PATCH 5/6] test(frameworks): document live query result oracles --- .../angular-db/tests/inject-live-query.test-d.ts | 5 +++++ packages/angular-db/tests/inject-live-query.test.ts | 6 ++++++ packages/db/tests/conformance/contract.ts | 12 +++++++++++- .../react-db/tests/useLiveInfiniteQuery.test-d.tsx | 7 +++++++ packages/react-db/tests/useLiveQuery.test-d.tsx | 6 ++++++ packages/solid-db/tests/useLiveQuery.test-d.tsx | 5 +++++ packages/solid-db/tests/useLiveQuery.test.tsx | 6 ++++++ packages/svelte-db/tests/useLiveQuery.svelte.test.ts | 6 ++++++ packages/svelte-db/tests/useLiveQuery.test-d.ts | 5 +++++ packages/vue-db/tests/useLiveQuery.test-d.ts | 5 +++++ packages/vue-db/tests/useLiveQuery.test.ts | 6 ++++++ 11 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/angular-db/tests/inject-live-query.test-d.ts b/packages/angular-db/tests/inject-live-query.test-d.ts index 358cc88c40..1680128016 100644 --- a/packages/angular-db/tests/inject-live-query.test-d.ts +++ b/packages/angular-db/tests/inject-live-query.test-d.ts @@ -146,6 +146,9 @@ describe(`injectLiveQuery type assertions`, () => { ) const enabled = null as unknown as boolean + // Compile-time observation cut: the public signal accessors returned by + // `injectLiveQuery`; the preload error proves the live-query Collection is + // absent while disabled. const result = injectLiveQuery((q) => enabled ? q.from({ collection }) : undefined, ) @@ -173,6 +176,8 @@ describe(`injectLiveQuery type assertions`, () => { ) const enabled = null as unknown as boolean + // The exact public result combines enabled `findOne` cardinality with the + // empty-reactive disabled value. A paired framework test owns transitions. const result = injectLiveQuery((q) => enabled ? q.from({ collection }).findOne() : null, ) diff --git a/packages/angular-db/tests/inject-live-query.test.ts b/packages/angular-db/tests/inject-live-query.test.ts index 1618d32fea..1c90a76025 100644 --- a/packages/angular-db/tests/inject-live-query.test.ts +++ b/packages/angular-db/tests/inject-live-query.test.ts @@ -1209,6 +1209,12 @@ describe(`injectLiveQuery`, () => { }) }) + /** + * Driver: public `injectLiveQuery` with an Angular signal. Each + * `waitForAngularUpdate` is an observation cut after disabled, enabled, and + * disabled-again updates. This test observes status and public result data; + * array-query Collection/state behavior remains in shared conformance. + */ it(`keeps conditional findOne data empty while disabled`, async () => { await TestBed.runInInjectionContext(async () => { const collection = createCollection( diff --git a/packages/db/tests/conformance/contract.ts b/packages/db/tests/conformance/contract.ts index 0655c3bcf1..3b8eb9aa8a 100644 --- a/packages/db/tests/conformance/contract.ts +++ b/packages/db/tests/conformance/contract.ts @@ -110,7 +110,17 @@ export interface ControllableHandle

extends LiveQueryHandle { setParam: (param: P) => Promise } -/** What each adapter package implements and hands to `runSuite`. */ +/** + * Shared live-query binding law: an enabled array query exposes row data from + * its live-query Collection; `findOne` exposes one row or `undefined`. A + * disabled callback has no live-query Collection, reports `disabled` status, + * and exposes the adapter's declared `absent` or `empty-reactive` public result. + * + * Each adapter implements this driver through its real public hook. `flush` + * defines the observation cut after framework updates and core sync settle. + * The runtime suite observes public result data, state, and status. Framework + * type inference and framework scheduler internals are outside this contract. + */ export interface LiveQueryDriver { name: string /** Public disabled data/state policy, not inferred from observed output. */ diff --git a/packages/react-db/tests/useLiveInfiniteQuery.test-d.tsx b/packages/react-db/tests/useLiveInfiniteQuery.test-d.tsx index cd9aa4db8d..a196313acf 100644 --- a/packages/react-db/tests/useLiveInfiniteQuery.test-d.tsx +++ b/packages/react-db/tests/useLiveInfiniteQuery.test-d.tsx @@ -28,6 +28,13 @@ describe(`useLiveInfiniteQuery type assertions`, () => { >().toEqualTypeOf<() => Promise>() }) + /** + * Law and source: the public pre-created live-query Collection overload must + * preserve its row, key, and utility types through `useLiveInfiniteQuery`. + * The compile-time observation cut is the returned public result. Missing-row + * and wrong-key accesses are hostile controls. Pagination timing and runtime + * page contents remain in the infinite-query conformance suite. + */ it(`preserves pre-created collection row, key, and utility types`, () => { type Post = { id: `post-${number}`; title: string } type PostKey = Post[`id`] diff --git a/packages/react-db/tests/useLiveQuery.test-d.tsx b/packages/react-db/tests/useLiveQuery.test-d.tsx index bcdc570700..7663306638 100644 --- a/packages/react-db/tests/useLiveQuery.test-d.tsx +++ b/packages/react-db/tests/useLiveQuery.test-d.tsx @@ -178,6 +178,12 @@ describe(`useLiveQuery type assertions`, () => { expectTypeOf(result.current.isEnabled).toEqualTypeOf() }) + /** + * React's public disabled result is absent rather than empty-reactive. The + * observation cut is `result.current` from the real `useLiveQuery` hook; the + * negative `map` call rejects an array-only disabled type. Runtime lifecycle + * and scheduler behavior remain in React conformance tests. + */ it(`types disabled callbacks with React's absent result representation`, () => { const collection = createCollection( mockSyncCollectionOptions({ diff --git a/packages/solid-db/tests/useLiveQuery.test-d.tsx b/packages/solid-db/tests/useLiveQuery.test-d.tsx index 2ee88d2112..e8fc5c4a2b 100644 --- a/packages/solid-db/tests/useLiveQuery.test-d.tsx +++ b/packages/solid-db/tests/useLiveQuery.test-d.tsx @@ -98,6 +98,9 @@ describe(`useLiveQuery type assertions`, () => { ) const enabled = null as unknown as boolean + // Compile-time observation cut: the public accessors returned by the real + // `useLiveQuery` hook; the preload error proves the live-query Collection + // is absent while disabled. const rendered = renderHook(() => useLiveQuery((q) => (enabled ? q.from({ collection }) : null)), ) @@ -127,6 +130,8 @@ describe(`useLiveQuery type assertions`, () => { ) const enabled = null as unknown as boolean + // The exact public result combines enabled `findOne` cardinality with the + // empty-reactive disabled value. A paired framework test owns transitions. const rendered = renderHook(() => useLiveQuery((q) => enabled ? q.from({ collection }).findOne() : undefined, diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 3883c1bde9..41e0c09ca1 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -2078,6 +2078,12 @@ describe(`Query Collections`, () => { }) }) + /** + * Driver: public `useLiveQuery` with a Solid signal. Initial reads and each + * `waitFor` completion are observation cuts for disabled, enabled, and + * disabled-again public results. Collection/state behavior remains in + * shared conformance; this test isolates conditional `findOne` data. + */ it(`keeps conditional findOne data empty while disabled`, async () => { return createRoot(async (dispose) => { const collection = createCollection( diff --git a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts index a956b03e6b..a4f8d9e41e 100644 --- a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts +++ b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts @@ -2187,6 +2187,12 @@ describe(`Query Collections`, () => { }) }) + /** + * Driver: public `useLiveQuery` with Svelte state. Each `flushSync` is the + * observation cut after disabled, enabled, and disabled-again updates. This + * test observes conditional `findOne` data and status; shared conformance + * owns array-query Collection/state behavior. + */ it(`keeps conditional findOne data empty while disabled`, () => { const collection = createCollection( mockSyncCollectionOptions({ diff --git a/packages/svelte-db/tests/useLiveQuery.test-d.ts b/packages/svelte-db/tests/useLiveQuery.test-d.ts index 51c731be37..8314badb6f 100644 --- a/packages/svelte-db/tests/useLiveQuery.test-d.ts +++ b/packages/svelte-db/tests/useLiveQuery.test-d.ts @@ -23,6 +23,9 @@ describe(`useLiveQuery type assertions`, () => { ) const enabled = null as unknown as boolean + // Compile-time observation cut: the public properties returned by the real + // `useLiveQuery` hook; the preload error proves the live-query Collection + // is absent while disabled. const result = useLiveQuery((q) => enabled ? q.from({ collection }) : undefined, ) @@ -65,6 +68,8 @@ describe(`useLiveQuery type assertions`, () => { ) const enabled = null as unknown as boolean + // The exact public result combines enabled `findOne` cardinality with the + // empty-reactive disabled value. A paired framework test owns transitions. const result = useLiveQuery((q) => enabled ? q.from({ collection }).findOne() : null, ) diff --git a/packages/vue-db/tests/useLiveQuery.test-d.ts b/packages/vue-db/tests/useLiveQuery.test-d.ts index 30594c15ef..5dce13c050 100644 --- a/packages/vue-db/tests/useLiveQuery.test-d.ts +++ b/packages/vue-db/tests/useLiveQuery.test-d.ts @@ -164,6 +164,9 @@ describe(`useLiveQuery type assertions`, () => { ) const enabled = null as unknown as boolean + // Compile-time observation cut: the public refs returned by the real + // `useLiveQuery` hook; the preload error proves the live-query Collection + // is absent while disabled. const result = useLiveQuery((q) => enabled ? q.from({ collection }) : null, ) @@ -198,6 +201,8 @@ describe(`useLiveQuery type assertions`, () => { ? TContext : never + // The exact public result combines enabled `findOne` cardinality with the + // empty-reactive disabled value. A paired framework test owns transitions. const result = useLiveQuery((q) => (enabled ? build(q) : null)) const annotated: ConditionalUseLiveQueryReturn = result diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index eb654b955c..037028fe22 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -1934,6 +1934,12 @@ describe(`Query Collections`, () => { expect(result.isReady.value).toBe(true) }) + /** + * Driver: public `useLiveQuery` with a Vue ref. The initial read and each + * `waitFor` completion are observation cuts for disabled, enabled, and + * disabled-again public results. Collection/state behavior remains in + * shared conformance; this test isolates conditional `findOne` data. + */ it(`keeps conditional findOne data empty while disabled`, async () => { const collection = createCollection( mockSyncCollectionOptions({ From 04bfcf30e3981cee63d1bcaa2116b43aab52f4f1 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 22 Sep 2026 12:41:58 +0100 Subject: [PATCH 6/6] test(frameworks): assert live query results directly --- packages/angular-db/tests/inject-live-query.test-d.ts | 5 +++-- packages/react-db/tests/useLiveQuery.test-d.tsx | 8 +++----- packages/solid-db/tests/useLiveQuery.test-d.tsx | 5 +++-- packages/svelte-db/tests/useLiveQuery.test-d.ts | 5 +++-- packages/vue-db/tests/useLiveQuery.test-d.ts | 5 +++-- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/angular-db/tests/inject-live-query.test-d.ts b/packages/angular-db/tests/inject-live-query.test-d.ts index 1680128016..8c46515707 100644 --- a/packages/angular-db/tests/inject-live-query.test-d.ts +++ b/packages/angular-db/tests/inject-live-query.test-d.ts @@ -153,8 +153,9 @@ describe(`injectLiveQuery type assertions`, () => { enabled ? q.from({ collection }) : undefined, ) - const data: Array> = result.data() - expectTypeOf(data).toEqualTypeOf>>() + expectTypeOf(result.data()).toEqualTypeOf< + Array>> + >() expectTypeOf(result.collection()).toEqualTypeOf>, string | number, diff --git a/packages/react-db/tests/useLiveQuery.test-d.tsx b/packages/react-db/tests/useLiveQuery.test-d.tsx index 7663306638..8148d2f832 100644 --- a/packages/react-db/tests/useLiveQuery.test-d.tsx +++ b/packages/react-db/tests/useLiveQuery.test-d.tsx @@ -18,7 +18,7 @@ import type { DbClient, DehydratedDbState } from '../../db/src/index' import type { JSX } from 'react' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' -import type { QueryBuilder } from '../../db/src/query/index' +import type { Prettify, QueryBuilder } from '../../db/src/query/index' import type { ConditionalUseLiveQueryConfig, UseLiveQueryConfig, @@ -198,10 +198,8 @@ describe(`useLiveQuery type assertions`, () => { useLiveQuery((q) => (enabled ? q.from({ collection }) : null)), ) - const data: Array> | undefined = - result.current.data - expectTypeOf(data).toEqualTypeOf< - Array> | undefined + expectTypeOf(result.current.data).toEqualTypeOf< + Array>> | undefined >() expectTypeOf(result.current.status).toEqualTypeOf() expectTypeOf(result.current.isEnabled).toEqualTypeOf() diff --git a/packages/solid-db/tests/useLiveQuery.test-d.tsx b/packages/solid-db/tests/useLiveQuery.test-d.tsx index e8fc5c4a2b..44b4ed8f77 100644 --- a/packages/solid-db/tests/useLiveQuery.test-d.tsx +++ b/packages/solid-db/tests/useLiveQuery.test-d.tsx @@ -105,8 +105,9 @@ describe(`useLiveQuery type assertions`, () => { useLiveQuery((q) => (enabled ? q.from({ collection }) : null)), ) - const data: Array> = rendered.result() - expectTypeOf(data).toEqualTypeOf>>() + expectTypeOf(rendered.result()).toEqualTypeOf< + Array>> + >() expectTypeOf(rendered.result.collection).toEqualTypeOf>, string | number, diff --git a/packages/svelte-db/tests/useLiveQuery.test-d.ts b/packages/svelte-db/tests/useLiveQuery.test-d.ts index 8314badb6f..67ff608b11 100644 --- a/packages/svelte-db/tests/useLiveQuery.test-d.ts +++ b/packages/svelte-db/tests/useLiveQuery.test-d.ts @@ -30,8 +30,9 @@ describe(`useLiveQuery type assertions`, () => { enabled ? q.from({ collection }) : undefined, ) - const data: Array> = result.data - expectTypeOf(data).toEqualTypeOf>>() + expectTypeOf(result.data).toEqualTypeOf< + Array>> + >() expectTypeOf(result.collection).toEqualTypeOf>, string | number, diff --git a/packages/vue-db/tests/useLiveQuery.test-d.ts b/packages/vue-db/tests/useLiveQuery.test-d.ts index 5dce13c050..79599c7fac 100644 --- a/packages/vue-db/tests/useLiveQuery.test-d.ts +++ b/packages/vue-db/tests/useLiveQuery.test-d.ts @@ -171,8 +171,9 @@ describe(`useLiveQuery type assertions`, () => { enabled ? q.from({ collection }) : null, ) - const data: Array> = result.data.value - expectTypeOf(data).toEqualTypeOf>>() + expectTypeOf(result.data.value).toEqualTypeOf< + Array>> + >() expectTypeOf(result.collection.value).toEqualTypeOf>, string | number,