From c24dd71c9661cfe012b4fdcea7c967649d161996 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 18:39:54 +0000 Subject: [PATCH 1/7] Fix: Allow useLiveSuspenseQuery to accept undefined to disable query - Add overloads to support query functions that can return undefined/null - Update implementation to return undefined values instead of throwing error when query is disabled - Add tests for conditional query pattern with undefined - Mirrors useLiveQuery behavior for consistency This allows conditional queries like: ```ts useLiveSuspenseQuery( (q) => userId ? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne() : undefined, [userId] ) ``` --- packages/react-db/src/useLiveSuspenseQuery.ts | 99 +++++++++++++++++-- .../tests/useLiveSuspenseQuery.test.tsx | 79 ++++++++++++--- 2 files changed, 160 insertions(+), 18 deletions(-) diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index 67163bdc7..4c485e288 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -71,6 +71,21 @@ import type { * * ) * } + * + * @example + * // Conditional query - return undefined to disable + * function UserProfile({ userId }: { userId?: string }) { + * const { data } = useLiveSuspenseQuery( + * (q) => userId ? q.from({ users: usersCollection }) + * .where(({ users }) => eq(users.id, userId)) + * .findOne() + * : undefined, + * [userId] + * ) + * + * if (!data) return
No user selected
+ * return
{data.name}
+ * } */ // Overload 1: Accept query function that always returns QueryBuilder export function useLiveSuspenseQuery( @@ -82,7 +97,75 @@ export function useLiveSuspenseQuery( collection: Collection, string | number, {}> } -// Overload 2: Accept config object +// Overload 2: Accept query function that can return undefined/null +export function useLiveSuspenseQuery( + queryFn: ( + q: InitialQueryBuilder + ) => QueryBuilder | undefined | null, + deps?: Array +): { + state: Map> | undefined + data: InferResultType | undefined + collection: Collection, string | number, {}> | undefined +} + +// Overload 3: Accept query function that can return LiveQueryCollectionConfig +export function useLiveSuspenseQuery( + queryFn: ( + q: InitialQueryBuilder + ) => LiveQueryCollectionConfig | undefined | null, + deps?: Array +): { + state: Map> | undefined + data: InferResultType | undefined + collection: Collection, string | number, {}> | undefined +} + +// Overload 4: Accept query function that can return Collection +export function useLiveSuspenseQuery< + TResult extends object, + TKey extends string | number, + TUtils extends Record, +>( + queryFn: ( + q: InitialQueryBuilder + ) => Collection | undefined | null, + deps?: Array +): { + state: Map | undefined + data: Array | undefined + collection: Collection | undefined +} + +// Overload 5: Accept query function that can return all types +export function useLiveSuspenseQuery< + TContext extends Context, + TResult extends object, + TKey extends string | number, + TUtils extends Record, +>( + queryFn: ( + q: InitialQueryBuilder + ) => + | QueryBuilder + | LiveQueryCollectionConfig + | Collection + | undefined + | null, + deps?: Array +): { + state: + | Map> + | Map + | undefined + data: InferResultType | Array | undefined + collection: + | Collection, string | number, {}> + | Collection + | undefined +} + +// Overload 6: Accept config object export function useLiveSuspenseQuery( config: LiveQueryCollectionConfig, deps?: Array @@ -92,7 +175,7 @@ export function useLiveSuspenseQuery( collection: Collection, string | number, {}> } -// Overload 3: Accept pre-created live query collection +// Overload 7: Accept pre-created live query collection export function useLiveSuspenseQuery< TResult extends object, TKey extends string | number, @@ -105,7 +188,7 @@ export function useLiveSuspenseQuery< collection: Collection } -// Overload 4: Accept pre-created live query collection with singleResult: true +// Overload 8: Accept pre-created live query collection with singleResult: true export function useLiveSuspenseQuery< TResult extends object, TKey extends string | number, @@ -146,10 +229,12 @@ export function useLiveSuspenseQuery( // SUSPENSE LOGIC: Throw promise or error based on collection status // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!result.isEnabled) { - // Suspense queries cannot be disabled - throw error - throw new Error( - `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.` - ) + // Query is disabled (callback returned undefined/null) - return undefined values without suspending + return { + state: undefined, + data: undefined, + collection: undefined, + } } // Only throw errors during initial load (before first ready) diff --git a/packages/react-db/tests/useLiveSuspenseQuery.test.tsx b/packages/react-db/tests/useLiveSuspenseQuery.test.tsx index 36a878bf9..be337cbbe 100644 --- a/packages/react-db/tests/useLiveSuspenseQuery.test.tsx +++ b/packages/react-db/tests/useLiveSuspenseQuery.test.tsx @@ -262,17 +262,74 @@ describe(`useLiveSuspenseQuery`, () => { }) }) - it(`should throw error when query function returns undefined`, () => { - expect(() => { - renderHook( - () => { - return useLiveSuspenseQuery(() => undefined as any) - }, - { - wrapper: SuspenseWrapper, - } - ) - }).toThrow(/does not support disabled queries/) + it(`should return undefined values when query function returns undefined`, async () => { + const { result } = renderHook( + () => { + return useLiveSuspenseQuery(() => undefined as any) + }, + { + wrapper: SuspenseWrapper, + } + ) + + // Should not suspend, just return undefined immediately + expect(result.current.data).toBeUndefined() + expect(result.current.state).toBeUndefined() + expect(result.current.collection).toBeUndefined() + }) + + it(`should support conditional queries with undefined`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-conditional-suspense`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + const { result, rerender } = renderHook( + ({ userId }: { userId?: string }) => { + return useLiveSuspenseQuery( + (q) => + userId + ? q + .from({ persons: collection }) + .where(({ persons }) => eq(persons.id, userId)) + .findOne() + : undefined, + [userId] + ) + }, + { + wrapper: SuspenseWrapper, + initialProps: { userId: undefined }, + } + ) + + // When userId is undefined, should return undefined without suspending + expect(result.current.data).toBeUndefined() + expect(result.current.state).toBeUndefined() + expect(result.current.collection).toBeUndefined() + + // Change to valid userId - should suspend and load data + rerender({ userId: `1` }) + + await waitFor(() => { + expect(result.current.data).toBeDefined() + }) + + expect(result.current.data).toMatchObject({ + id: `1`, + name: `John Doe`, + age: 30, + }) + + // Change back to undefined - should return undefined again + rerender({ userId: undefined }) + + expect(result.current.data).toBeUndefined() + expect(result.current.state).toBeUndefined() + expect(result.current.collection).toBeUndefined() }) it(`should work with config object`, async () => { From 1290791a0abaa44fdc26a2a1fa7248e5072dba6b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 18:41:08 +0000 Subject: [PATCH 2/7] Add changeset for useLiveSuspenseQuery undefined support --- .../suspense-live-query-undefined-support.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .changeset/suspense-live-query-undefined-support.md diff --git a/.changeset/suspense-live-query-undefined-support.md b/.changeset/suspense-live-query-undefined-support.md new file mode 100644 index 000000000..ed83e8222 --- /dev/null +++ b/.changeset/suspense-live-query-undefined-support.md @@ -0,0 +1,35 @@ +--- +"@tanstack/react-db": patch +--- + +Allow `useLiveSuspenseQuery` to accept `undefined` to disable queries, matching `useLiveQuery` behavior. + +`useLiveSuspenseQuery` now supports conditional queries by accepting query functions that can return `undefined` or `null`. When the query function returns `undefined`, the hook returns `undefined` values without suspending, instead of throwing an error. + +**Before:** + +```typescript +// This would throw an error +useLiveSuspenseQuery( + (q) => userId + ? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne() + : undefined, + [userId] +) +// Error: useLiveSuspenseQuery does not support disabled queries +``` + +**After:** + +```typescript +// Now works correctly - returns undefined when userId is not set +const { data } = useLiveSuspenseQuery( + (q) => userId + ? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne() + : undefined, + [userId] +) +// data is undefined when userId is undefined, without suspending +``` + +This change makes `useLiveSuspenseQuery` consistent with `useLiveQuery` and enables conditional query patterns that are common in React applications. From cf18e7381f5bf9ed4e346a4a6aeaf751c586459b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 18:48:01 +0000 Subject: [PATCH 3/7] Revert: useLiveSuspenseQuery should not support disabled queries Following TanStack Query's useSuspenseQuery design philosophy, disabled queries are intentionally not supported to maintain the type guarantee that data is T (not T | undefined). Changes: - Revert type overloads that allowed undefined/null returns - Keep error throw when query callback returns undefined/null - Improve error message with clear guidance on alternatives: 1. Use conditional rendering (don't render component until ready) 2. Use useLiveQuery instead (supports isEnabled flag) - Update tests to expect error instead of undefined values - Update changeset to document the design decision and alternatives This matches TanStack Query's approach where Suspense queries prioritize type safety and proper component composition over flexibility. --- .../suspense-live-query-undefined-support.md | 46 +++++--- packages/react-db/src/useLiveSuspenseQuery.ts | 102 ++---------------- .../tests/useLiveSuspenseQuery.test.tsx | 79 ++------------ 3 files changed, 55 insertions(+), 172 deletions(-) diff --git a/.changeset/suspense-live-query-undefined-support.md b/.changeset/suspense-live-query-undefined-support.md index ed83e8222..475020232 100644 --- a/.changeset/suspense-live-query-undefined-support.md +++ b/.changeset/suspense-live-query-undefined-support.md @@ -2,34 +2,54 @@ "@tanstack/react-db": patch --- -Allow `useLiveSuspenseQuery` to accept `undefined` to disable queries, matching `useLiveQuery` behavior. +Improve error message when `useLiveSuspenseQuery` receives `undefined` from query callback. -`useLiveSuspenseQuery` now supports conditional queries by accepting query functions that can return `undefined` or `null`. When the query function returns `undefined`, the hook returns `undefined` values without suspending, instead of throwing an error. +Following TanStack Query's `useSuspenseQuery` design, `useLiveSuspenseQuery` intentionally does not support disabled queries (when callback returns `undefined` or `null`). This maintains the type guarantee that `data` is always `T` (not `T | undefined`), which is a core benefit of using Suspense. -**Before:** +**What changed:** + +The error message is now more helpful and explains the design decision: + +``` +useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). +The Suspense pattern requires data to always be defined (T, not T | undefined). +Solutions: +1) Use conditional rendering - don't render the component until the condition is met. +2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag. +``` + +**Why this matters:** ```typescript -// This would throw an error -useLiveSuspenseQuery( +// ❌ This pattern doesn't work with Suspense queries: +const { data } = useLiveSuspenseQuery( (q) => userId ? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne() : undefined, [userId] ) -// Error: useLiveSuspenseQuery does not support disabled queries -``` -**After:** +// ✅ Instead, use conditional rendering: +function UserProfile({ userId }: { userId: string }) { + const { data } = useLiveSuspenseQuery( + (q) => q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne(), + [userId] + ) + return
{data.name}
// data is guaranteed non-undefined +} -```typescript -// Now works correctly - returns undefined when userId is not set -const { data } = useLiveSuspenseQuery( +function App({ userId }: { userId?: string }) { + if (!userId) return
No user selected
+ return +} + +// ✅ Or use useLiveQuery for conditional queries: +const { data, isEnabled } = useLiveQuery( (q) => userId ? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne() : undefined, [userId] ) -// data is undefined when userId is undefined, without suspending ``` -This change makes `useLiveSuspenseQuery` consistent with `useLiveQuery` and enables conditional query patterns that are common in React applications. +This aligns with TanStack Query's philosophy where Suspense queries prioritize type safety and proper component composition over flexibility. diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index 4c485e288..8ff519dd3 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -72,20 +72,6 @@ import type { * ) * } * - * @example - * // Conditional query - return undefined to disable - * function UserProfile({ userId }: { userId?: string }) { - * const { data } = useLiveSuspenseQuery( - * (q) => userId ? q.from({ users: usersCollection }) - * .where(({ users }) => eq(users.id, userId)) - * .findOne() - * : undefined, - * [userId] - * ) - * - * if (!data) return
No user selected
- * return
{data.name}
- * } */ // Overload 1: Accept query function that always returns QueryBuilder export function useLiveSuspenseQuery( @@ -97,75 +83,7 @@ export function useLiveSuspenseQuery( collection: Collection, string | number, {}> } -// Overload 2: Accept query function that can return undefined/null -export function useLiveSuspenseQuery( - queryFn: ( - q: InitialQueryBuilder - ) => QueryBuilder | undefined | null, - deps?: Array -): { - state: Map> | undefined - data: InferResultType | undefined - collection: Collection, string | number, {}> | undefined -} - -// Overload 3: Accept query function that can return LiveQueryCollectionConfig -export function useLiveSuspenseQuery( - queryFn: ( - q: InitialQueryBuilder - ) => LiveQueryCollectionConfig | undefined | null, - deps?: Array -): { - state: Map> | undefined - data: InferResultType | undefined - collection: Collection, string | number, {}> | undefined -} - -// Overload 4: Accept query function that can return Collection -export function useLiveSuspenseQuery< - TResult extends object, - TKey extends string | number, - TUtils extends Record, ->( - queryFn: ( - q: InitialQueryBuilder - ) => Collection | undefined | null, - deps?: Array -): { - state: Map | undefined - data: Array | undefined - collection: Collection | undefined -} - -// Overload 5: Accept query function that can return all types -export function useLiveSuspenseQuery< - TContext extends Context, - TResult extends object, - TKey extends string | number, - TUtils extends Record, ->( - queryFn: ( - q: InitialQueryBuilder - ) => - | QueryBuilder - | LiveQueryCollectionConfig - | Collection - | undefined - | null, - deps?: Array -): { - state: - | Map> - | Map - | undefined - data: InferResultType | Array | undefined - collection: - | Collection, string | number, {}> - | Collection - | undefined -} - -// Overload 6: Accept config object +// Overload 2: Accept config object export function useLiveSuspenseQuery( config: LiveQueryCollectionConfig, deps?: Array @@ -175,7 +93,7 @@ export function useLiveSuspenseQuery( collection: Collection, string | number, {}> } -// Overload 7: Accept pre-created live query collection +// Overload 3: Accept pre-created live query collection export function useLiveSuspenseQuery< TResult extends object, TKey extends string | number, @@ -188,7 +106,7 @@ export function useLiveSuspenseQuery< collection: Collection } -// Overload 8: Accept pre-created live query collection with singleResult: true +// Overload 4: Accept pre-created live query collection with singleResult: true export function useLiveSuspenseQuery< TResult extends object, TKey extends string | number, @@ -229,12 +147,14 @@ export function useLiveSuspenseQuery( // SUSPENSE LOGIC: Throw promise or error based on collection status // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!result.isEnabled) { - // Query is disabled (callback returned undefined/null) - return undefined values without suspending - return { - state: undefined, - data: undefined, - collection: undefined, - } + // Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior + throw new Error( + `useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). ` + + `The Suspense pattern requires data to always be defined (T, not T | undefined). ` + + `Solutions: ` + + `1) Use conditional rendering - don't render the component until the condition is met. ` + + `2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.` + ) } // Only throw errors during initial load (before first ready) diff --git a/packages/react-db/tests/useLiveSuspenseQuery.test.tsx b/packages/react-db/tests/useLiveSuspenseQuery.test.tsx index be337cbbe..36a878bf9 100644 --- a/packages/react-db/tests/useLiveSuspenseQuery.test.tsx +++ b/packages/react-db/tests/useLiveSuspenseQuery.test.tsx @@ -262,74 +262,17 @@ describe(`useLiveSuspenseQuery`, () => { }) }) - it(`should return undefined values when query function returns undefined`, async () => { - const { result } = renderHook( - () => { - return useLiveSuspenseQuery(() => undefined as any) - }, - { - wrapper: SuspenseWrapper, - } - ) - - // Should not suspend, just return undefined immediately - expect(result.current.data).toBeUndefined() - expect(result.current.state).toBeUndefined() - expect(result.current.collection).toBeUndefined() - }) - - it(`should support conditional queries with undefined`, async () => { - const collection = createCollection( - mockSyncCollectionOptions({ - id: `test-persons-conditional-suspense`, - getKey: (person: Person) => person.id, - initialData: initialPersons, - }) - ) - - const { result, rerender } = renderHook( - ({ userId }: { userId?: string }) => { - return useLiveSuspenseQuery( - (q) => - userId - ? q - .from({ persons: collection }) - .where(({ persons }) => eq(persons.id, userId)) - .findOne() - : undefined, - [userId] - ) - }, - { - wrapper: SuspenseWrapper, - initialProps: { userId: undefined }, - } - ) - - // When userId is undefined, should return undefined without suspending - expect(result.current.data).toBeUndefined() - expect(result.current.state).toBeUndefined() - expect(result.current.collection).toBeUndefined() - - // Change to valid userId - should suspend and load data - rerender({ userId: `1` }) - - await waitFor(() => { - expect(result.current.data).toBeDefined() - }) - - expect(result.current.data).toMatchObject({ - id: `1`, - name: `John Doe`, - age: 30, - }) - - // Change back to undefined - should return undefined again - rerender({ userId: undefined }) - - expect(result.current.data).toBeUndefined() - expect(result.current.state).toBeUndefined() - expect(result.current.collection).toBeUndefined() + it(`should throw error when query function returns undefined`, () => { + expect(() => { + renderHook( + () => { + return useLiveSuspenseQuery(() => undefined as any) + }, + { + wrapper: SuspenseWrapper, + } + ) + }).toThrow(/does not support disabled queries/) }) it(`should work with config object`, async () => { From 4061ba8fd12a12d90bc2e3017ecd382c0e45f3e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 18:58:46 +0000 Subject: [PATCH 4/7] Add comprehensive JSDoc documentation for useLiveSuspenseQuery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add detailed @remarks section that appears in IDE tooltips to clearly explain that disabled queries are not supported and provide two alternative solutions: 1. Use conditional rendering (recommended pattern) 2. Use useLiveQuery instead (supports isEnabled flag) Includes clear examples showing both ❌ incorrect and ✅ correct patterns. This provides immediate guidance when users encounter the type error, without requiring complex type-level error messages that can be fragile. TypeScript's natural "No overload matches this call" error combined with the JSDoc tooltip provides a good developer experience. --- packages/react-db/src/useLiveSuspenseQuery.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index 8ff519dd3..f96fe03bd 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -72,6 +72,38 @@ import type { * ) * } * + * @remarks + * **Important:** This hook does NOT support disabled queries (returning undefined/null). + * Following TanStack Query's useSuspenseQuery design, the query callback must always + * return a valid query, collection, or config object. + * + * ❌ **This will cause a type error:** + * ```ts + * useLiveSuspenseQuery( + * (q) => userId ? q.from({ users }) : undefined // ❌ Error! + * ) + * ``` + * + * ✅ **Use conditional rendering instead:** + * ```ts + * function Profile({ userId }: { userId: string }) { + * const { data } = useLiveSuspenseQuery( + * (q) => q.from({ users }).where(({ users }) => eq(users.id, userId)) + * ) + * return
{data.name}
+ * } + * + * // In parent component: + * {userId ? :
No user
} + * ``` + * + * ✅ **Or use useLiveQuery for conditional queries:** + * ```ts + * const { data, isEnabled } = useLiveQuery( + * (q) => userId ? q.from({ users }) : undefined, // ✅ Supported! + * [userId] + * ) + * ``` */ // Overload 1: Accept query function that always returns QueryBuilder export function useLiveSuspenseQuery( From d5c7c2241f16ffc0733fafa965488ec7205bb4d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 19:07:43 +0000 Subject: [PATCH 5/7] WIP: Experiment with unconstructable types for custom type errors Add 'poison pill' overloads that return DisabledQueryError type with custom error message embedded via unique symbol. This is experimental to evaluate if it provides better DX than JSDoc alone. Still evaluating trade-offs before finalizing approach. --- packages/react-db/src/useLiveSuspenseQuery.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index f96fe03bd..6d4bd80ee 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -12,6 +12,14 @@ import type { SingleResult, } from "@tanstack/db" +// Unique symbol for type-level error messages +declare const TypeException: unique symbol + +// Custom compile-time error for disabled queries +type DisabledQueryError = { + [TypeException]: `❌ useLiveSuspenseQuery does not support disabled queries (returning undefined/null).\n\n✅ Solution 1: Use conditional rendering\n Don't render this component until data is ready:\n {userId ? :
No user
}\n\n✅ Solution 2: Use useLiveQuery instead\n It supports the 'isEnabled' flag for conditional queries:\n useLiveQuery((q) => userId ? q.from(...) : undefined, [userId])` +} + /** * Create a live query with React Suspense support * @param queryFn - Query function that defines what data to fetch @@ -105,6 +113,32 @@ import type { * ) * ``` */ +// "Poison pill" overloads - catch disabled queries and show custom compile-time error +export function useLiveSuspenseQuery( + queryFn: ( + q: InitialQueryBuilder + ) => QueryBuilder | undefined | null, + deps?: Array +): DisabledQueryError + +export function useLiveSuspenseQuery( + queryFn: ( + q: InitialQueryBuilder + ) => LiveQueryCollectionConfig | undefined | null, + deps?: Array +): DisabledQueryError + +export function useLiveSuspenseQuery< + TResult extends object, + TKey extends string | number, + TUtils extends Record, +>( + queryFn: ( + q: InitialQueryBuilder + ) => Collection | undefined | null, + deps?: Array +): DisabledQueryError + // Overload 1: Accept query function that always returns QueryBuilder export function useLiveSuspenseQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, From 01c9afc7ed8bdd563be626f43ed485832363a4c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 19:25:08 +0000 Subject: [PATCH 6/7] Success: Unconstructable types work with proper implementation signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The key insight: make the implementation signature return type a union that includes both DisabledQueryError and the valid return types. TypeScript requires overload signatures to be compatible with the implementation signature. By using a union type: DisabledQueryError | { state: any; data: any; collection: any } We satisfy TypeScript's compatibility requirement while still catching invalid patterns at compile time. What users experience: 1. Type error when returning undefined → DisabledQueryError inferred 2. Property access errors: "Property 'data' does not exist on type 'DisabledQueryError'" 3. IDE tooltip shows custom error message embedded in the type 4. Compilation fails (forces fix) This provides BOTH: - JSDoc documentation (in tooltips) - Active type-level errors with custom messaging --- packages/react-db/src/useLiveSuspenseQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index 6d4bd80ee..9c372b121 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -189,7 +189,7 @@ export function useLiveSuspenseQuery< export function useLiveSuspenseQuery( configOrQueryOrCollection: any, deps: Array = [] -) { +): DisabledQueryError | { state: any; data: any; collection: any } { const promiseRef = useRef | null>(null) const collectionRef = useRef | null>(null) const hasBeenReadyRef = useRef(false) From fbed8640a7e3d0af8fd0dc3d568c10eee713391d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Nov 2025 22:42:22 +0000 Subject: [PATCH 7/7] Fix: Reorder overloads to fix type inference The poison pill overloads were matching BEFORE the specific overloads, causing TypeScript to infer DisabledQueryError for valid queries. TypeScript checks overloads top-to-bottom and uses the first match. Since QueryBuilder is assignable to QueryBuilder | undefined | null, the poison pill overloads were matching first. Solution: Move poison pill overloads to the END, just before the implementation. This ensures: 1. Specific overloads (without undefined) match first 2. Poison pill overloads (with undefined) only match when needed All tests now pass with no type errors. --- packages/react-db/src/useLiveSuspenseQuery.ts | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index 9c372b121..4604ea29a 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -113,32 +113,6 @@ type DisabledQueryError = { * ) * ``` */ -// "Poison pill" overloads - catch disabled queries and show custom compile-time error -export function useLiveSuspenseQuery( - queryFn: ( - q: InitialQueryBuilder - ) => QueryBuilder | undefined | null, - deps?: Array -): DisabledQueryError - -export function useLiveSuspenseQuery( - queryFn: ( - q: InitialQueryBuilder - ) => LiveQueryCollectionConfig | undefined | null, - deps?: Array -): DisabledQueryError - -export function useLiveSuspenseQuery< - TResult extends object, - TKey extends string | number, - TUtils extends Record, ->( - queryFn: ( - q: InitialQueryBuilder - ) => Collection | undefined | null, - deps?: Array -): DisabledQueryError - // Overload 1: Accept query function that always returns QueryBuilder export function useLiveSuspenseQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, @@ -185,6 +159,33 @@ export function useLiveSuspenseQuery< collection: Collection & SingleResult } +// "Poison pill" overloads - catch disabled queries and show custom compile-time error +// These MUST come AFTER the specific overloads so TypeScript tries them last +export function useLiveSuspenseQuery( + queryFn: ( + q: InitialQueryBuilder + ) => QueryBuilder | undefined | null, + deps?: Array +): DisabledQueryError + +export function useLiveSuspenseQuery( + queryFn: ( + q: InitialQueryBuilder + ) => LiveQueryCollectionConfig | undefined | null, + deps?: Array +): DisabledQueryError + +export function useLiveSuspenseQuery< + TResult extends object, + TKey extends string | number, + TUtils extends Record, +>( + queryFn: ( + q: InitialQueryBuilder + ) => Collection | undefined | null, + deps?: Array +): DisabledQueryError + // Implementation - uses useLiveQuery internally and adds Suspense logic export function useLiveSuspenseQuery( configOrQueryOrCollection: any,