-
Notifications
You must be signed in to change notification settings - Fork 112
Fix findOne type issue with joins in useLiveQuery #749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix findOne type issue with joins in useLiveQuery #749
Conversation
## Problem
When using findOne() after join operations, the type of query.data
became 'never', breaking TypeScript type inference. This was reported
on Discord where users found that limit(1) worked but findOne() did not.
## Root Cause
In MergeContextWithJoinType, the singleResult property was being
explicitly set to false when not true:
singleResult: TContext['singleResult'] extends true ? true : false
This caused a type conflict when findOne() later tried to intersect
with SingleResult:
{ singleResult: false } & { singleResult: true } = { singleResult: never }
## Solution
Changed to preserve the singleResult value as-is:
singleResult: TContext['singleResult']
This allows:
- findOne() before join: singleResult stays true
- findOne() after join: singleResult is undefined, which properly
intersects with { singleResult: true }
## Changes
- Fixed type definition in packages/db/src/query/builder/types.ts:577
- Added 8 comprehensive type tests for findOne() with all join types
- Added investigation documentation
## Test Coverage
- findOne() with leftJoin, innerJoin, rightJoin, fullJoin
- findOne() with multiple joins
- findOne() with select
- findOne() before vs after joins
- limit(1) vs findOne() type differences
🦋 Changeset detectedLatest commit: e022e1d The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
More templates
@tanstack/angular-db
@tanstack/db
@tanstack/db-ivm
@tanstack/electric-db-collection
@tanstack/offline-transactions
@tanstack/powersync-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
|
Size Change: 0 B Total Size: 79.1 kB ℹ️ View Unchanged
|
|
Size Change: 0 B Total Size: 3.34 kB ℹ️ View Unchanged
|
## Problem When using findOne() after join operations, the type became 'never'. The original fix (commit 5613b5a) used `singleResult: TContext['singleResult']` but this still had edge case issues with type intersection. ## Root Cause The MergeContextWithJoinType was setting singleResult to an explicit value (true or false), which created conflicts when findOne() tried to intersect with { singleResult: true }. ## Solution Use a conditional intersection to either include { singleResult: true } or an empty object {}: ```typescript } & (TContext['singleResult'] extends true ? { singleResult: true } : {}) ``` This ensures: - When singleResult is true: it's preserved - When singleResult is not set: the property is completely absent (not undefined/false) - When findOne() is called after joins: {} & { singleResult: true } = { singleResult: true } ✅ ## Changes - Refined type definition in packages/db/src/query/builder/types.ts:577 - Simplified join.test-d.ts tests to use .not.toBeNever() assertions - Added direct Discord bug reproduction test - Updated investigation documentation ## Test Results ✅ All 1413 tests pass (74 test files) ✅ No type errors ✅ Verified with direct reproduction of Discord bug report
Renamed test file and updated descriptions to focus on functionality rather than the source of the bug report. These tests will live in the codebase long-term and should describe what they test, not where the bug was originally reported. Changes: - Renamed findone-joins-discord-bug.test-d.ts -> findone-joins.test-d.ts - Updated describe block: 'Discord Bug: findOne() with joins' -> 'findOne() with joins' - Updated test names to describe functionality being tested - Updated collection IDs to not reference discord - Updated comments to be more descriptive
Per technical review feedback:
1. Apply non-distributive conditional pattern
- Wrap conditional in tuple [TFlag] extends [true] to prevent
unexpected behavior with union types
- More robust against future type system changes
2. Extract named utility type (PreserveSingleResultFlag)
- Improves readability and makes intent crystal clear
- Easier for future maintainers to understand the pattern
3. Add comprehensive edge case tests
- rightJoin + findOne()
- fullJoin + findOne()
- Multiple joins + findOne()
- select() + findOne() + joins (both orders)
- Total: 5 new type tests added
All 1418 tests pass with no type errors.
Updated changeset to show: - The actual PreserveSingleResultFlag helper type - Non-distributive conditional pattern [TFlag] extends [true] - Accurate test count (8 new tests added) The changeset now matches the actual implementation in the codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good spot, and I think the implementation is correct. However I don't like that the tests are just asserting that its not never and not that it's of the correct type.
| // Verify query result is properly typed (not never) | ||
| type QueryData = typeof query.toArray | ||
| type IsNever<T> = [T] extends [never] ? true : false | ||
| type DataIsNever = IsNever<QueryData> | ||
|
|
||
| expectTypeOf<DataIsNever>().toEqualTypeOf<false>() | ||
| expectTypeOf(query.toArray).not.toBeNever() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we assert that the data is exactly the correct type, not that it's not never.
Replace .not.toBeNever() assertions with precise type checks that verify
the exact structure of query results. Now asserting that findOne() with
leftJoin returns Array<{ todo: Todo, todoOptions: TodoOption | undefined }>,
ensuring proper optionality is tested.
Addresses feedback from samwillis on PR #749 requesting tests assert
correct types rather than just checking they're not never.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
Run docs:generate to sync generated documentation with current codebase. Removes old doc files as part of the regeneration process. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Run pnpm install && pnpm build (db package) && tsx scripts/generateDocs.ts to properly generate docs from built TypeScript declaration files. Previous commit incorrectly deleted docs because build step was skipped. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
| @@ -918,3 +918,189 @@ describe(`Join with ArkType Schemas`, () => { | |||
| >() | |||
| }) | |||
| }) | |||
|
|
|||
| describe(`findOne() with joins - Type Safety`, () => { | |||
| test(`findOne() with leftJoin should not result in never type`, () => { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of the new tests in this file are still asserting not-never and have that in the name.
We also have the new findone-joins.test-d.ts file with what look like sensible tests, these may be redundant?
Update check-docs job to provide clear step-by-step instructions when generated docs are out of sync. The updated error message now explicitly shows that builds must happen before doc generation: 1. pnpm install 2. pnpm build (generates .d.ts files needed for docs) 3. pnpm docs:generate (uses built declaration files) This makes it clear that docs are generated from built TypeScript declaration files, not directly from source. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
15036b3 to
75fd5bd
Compare
# Conflicts: # .github/workflows/pr.yml # docs/reference/classes/AggregateFunctionNotInSelectError.md # docs/reference/classes/AggregateNotSupportedError.md # docs/reference/classes/CannotCombineEmptyExpressionListError.md # docs/reference/classes/CollectionConfigurationError.md # docs/reference/classes/CollectionInErrorStateError.md # docs/reference/classes/CollectionInputNotFoundError.md # docs/reference/classes/CollectionIsInErrorStateError.md # docs/reference/classes/CollectionOperationError.md # docs/reference/classes/CollectionRequiresConfigError.md # docs/reference/classes/CollectionRequiresSyncConfigError.md # docs/reference/classes/CollectionStateError.md # docs/reference/classes/DeleteKeyNotFoundError.md # docs/reference/classes/DistinctRequiresSelectError.md # docs/reference/classes/DuplicateKeyError.md # docs/reference/classes/DuplicateKeySyncError.md # docs/reference/classes/EmptyReferencePathError.md # docs/reference/classes/GroupByError.md # docs/reference/classes/HavingRequiresGroupByError.md # docs/reference/classes/InvalidCollectionStatusTransitionError.md # docs/reference/classes/InvalidJoinCondition.md # docs/reference/classes/InvalidJoinConditionLeftSourceError.md # docs/reference/classes/InvalidJoinConditionRightSourceError.md # docs/reference/classes/InvalidJoinConditionSameSourceError.md # docs/reference/classes/InvalidJoinConditionSourceMismatchError.md # docs/reference/classes/InvalidSchemaError.md # docs/reference/classes/InvalidSourceError.md # docs/reference/classes/InvalidStorageDataFormatError.md # docs/reference/classes/InvalidStorageObjectFormatError.md # docs/reference/classes/JoinCollectionNotFoundError.md # docs/reference/classes/JoinConditionMustBeEqualityError.md # docs/reference/classes/JoinError.md # docs/reference/classes/KeyUpdateNotAllowedError.md # docs/reference/classes/LimitOffsetRequireOrderByError.md # docs/reference/classes/LocalStorageCollectionError.md # docs/reference/classes/MissingAliasInputsError.md # docs/reference/classes/MissingDeleteHandlerError.md # docs/reference/classes/MissingHandlerError.md # docs/reference/classes/MissingInsertHandlerError.md # docs/reference/classes/MissingMutationFunctionError.md # docs/reference/classes/MissingUpdateArgumentError.md # docs/reference/classes/MissingUpdateHandlerError.md # docs/reference/classes/NegativeActiveSubscribersError.md # docs/reference/classes/NoKeysPassedToDeleteError.md # docs/reference/classes/NoKeysPassedToUpdateError.md # docs/reference/classes/NoPendingSyncTransactionCommitError.md # docs/reference/classes/NoPendingSyncTransactionWriteError.md # docs/reference/classes/NonAggregateExpressionNotInGroupByError.md # docs/reference/classes/OnlyOneSourceAllowedError.md # docs/reference/classes/QueryBuilderError.md # docs/reference/classes/QueryCompilationError.md # docs/reference/classes/QueryMustHaveFromClauseError.md # docs/reference/classes/QueryOptimizerError.md # docs/reference/classes/SchemaMustBeSynchronousError.md # docs/reference/classes/SerializationError.md # docs/reference/classes/SetWindowRequiresOrderByError.md # docs/reference/classes/StorageError.md # docs/reference/classes/StorageKeyRequiredError.md # docs/reference/classes/SubQueryMustHaveFromClauseError.md # docs/reference/classes/SubscriptionNotFoundError.md # docs/reference/classes/SyncCleanupError.md # docs/reference/classes/SyncTransactionAlreadyCommittedError.md # docs/reference/classes/SyncTransactionAlreadyCommittedWriteError.md # docs/reference/classes/TanStackDBError.md # docs/reference/classes/TransactionAlreadyCompletedRollbackError.md # docs/reference/classes/TransactionError.md # docs/reference/classes/TransactionNotPendingCommitError.md # docs/reference/classes/TransactionNotPendingMutateError.md # docs/reference/classes/UndefinedKeyError.md # docs/reference/classes/UnknownExpressionTypeError.md # docs/reference/classes/UnknownFunctionError.md # docs/reference/classes/UnknownHavingExpressionTypeError.md # docs/reference/classes/UnsupportedAggregateFunctionError.md # docs/reference/classes/UnsupportedFromTypeError.md # docs/reference/classes/UnsupportedJoinSourceTypeError.md # docs/reference/classes/UnsupportedJoinTypeError.md # docs/reference/classes/UpdateKeyNotFoundError.md # docs/reference/classes/WhereClauseConversionError.md # docs/reference/functions/compileQuery.md # docs/reference/functions/createOptimisticAction.md # docs/reference/functions/createPacedMutations.md # docs/reference/functions/createTransaction.md # docs/reference/functions/debounceStrategy.md # docs/reference/functions/getActiveTransaction.md # docs/reference/functions/throttleStrategy.md # docs/reference/index.md # docs/reference/interfaces/Transaction.md # docs/reference/type-aliases/GetResult.md # docs/reference/type-aliases/InferResultType.md
Regenerate docs after merging with main to reflect: - New useLiveSuspenseQuery hook from React Suspense support - Updated line numbers in GetResult and InferResultType from type changes - Removed PowerSync docs (package has build errors) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Expand findone-joins.test-d.ts with all findOne() + joins scenarios using exact type assertions (innerJoin, rightJoin, fullJoin, multiple joins, select combinations) - Remove redundant "findOne() with joins" section from join.test-d.ts that used weak .not.toBeNever() assertions - All findOne() join tests now verify exact type structures with proper optionality checks Addresses samwillis feedback on PR #749 about redundant tests and weak type assertions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
PowerSync package builds successfully - the earlier deletion was due to a transient build failure. Regenerated docs after clean install/build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
![]()
Fix type inference for findOne() when used with join operations
Previously, using
findOne()with join operations (leftJoin, innerJoin, etc.) resulted in the query type being inferred asnever, breaking TypeScript type checking:The Fix:
Fixed the
MergeContextWithJoinTypetype definition to conditionally include thesingleResultproperty only when it's explicitlytrue, avoiding type conflicts whenfindOne()is called after joins:Why This Works:
By using a conditional intersection that omits the property entirely when not needed, we avoid type conflicts. Intersecting
{} & { singleResult: true }cleanly results in{ singleResult: true }, whereas the previous approach created conflicting property types resulting innever. The tuple wrapper ([TFlag]) ensures robust behavior even if the flag type becomes a union in the future.Impact:
findOne()now works correctly with all join typesuseLiveQueryand other contextsfindOne()before and after joins work correctly