Skip to content

useQuery return type NoInfer preserves reverse-inference guard but breaks discriminated-union narrowing #11018

Description

@hsheth2

Context

useQuery currently returns UseQueryResult<NoInfer<TData>, TError>. That NoInfer guard is important: it prevents reverse inference from an annotated result type.

Without the guard, TypeScript can accept an incorrect annotation like this by letting the annotation influence TData:

const result: UseQueryResult<{ wow: string }> = useQuery({
  queryKey: ['key'],
  queryFn: () => ({ wow: true }),
  initialData: () => undefined as { wow: boolean } | undefined,
})

That should be a type error because the query options produce { wow: boolean }, not { wow: string }. That is the behavior fixed in #8683.

Describe the bug

The regression is that after #10593 switched this return type to TypeScript's built-in NoInfer, discriminated-union narrowing on data no longer works in normal code:

type Result =
  | { type: 'first'; first: string }
  | { type: 'second'; second: string }

const query = useQuery({
  queryKey: ['example'],
  queryFn: (): Result => ({ type: 'first', first: 'a' }),
})

const second = query.data?.type === 'first' ? undefined : query.data
second?.second

This is not asking to remove NoInfer. Removing it would regress the reverse-inference guard. The issue is that built-in NoInfer<TData> on the return type appears to preserve that guard while also blocking useful union narrowing.

Your minimal, reproducible example

https://www.typescriptlang.org/play/?ts=5.9.3#code/C4TwDgpgBAgg7gQwJbAgEwGoIDYFcIA8AKgHxQC8URUEAHqgHZoDOUACgE4D2Atks4SQMAZhA5QAqmQD8kqAC4qAKFCRJAgIr4OIAEoRmubMGIARBMARlKAbyhoLCRUXOWoAHyi4mEYUPRQAL4q4NAA8mDASFwMzMRaYiAAYgyuCAA0VGkUsIgo6Fh4hEQJOilpJNZQNkpQUACO2skMigAUAJQUZCVN5Y61UEIoSDhp0m2d5GTwyKiYOPjxvamOZJ7eaL7+aErBqtAAcggc3IgARtgQB1wAkiJixFXUdIwsXgwA1gxccAxQstc7qIOI8FFAGBAAG5iJRKTYAY2wx2gwm88KiMS8miaABkuFwBEtEn03JRvF8fgxMi5HDkZvl5kUiWUVpZKq0BlxItFYooIhjYszmmlqRV0kp2ooJNjEvpDMYzKs4RBEcioKiGOieViIKVkhBNeghSSEDlyd9fqLaZR6XNCosesTWVYSBy6lyBcw+dyYnFHSyRVlVuLJepdU05UYTID7iCaWySMrVRwUWiBTq9aZ+MAOEgzrg5sbnWbPhaqUHSblZgUFsU9Sb2ZyfbyoPyeX7686rWyQ1KZTpIwqjicfggLldbrHFQnYfDfcAoNh8QIAOrcBgAcz74dlBijBDscB+imYOaEG6CVVw-ZAeIJEFaNTqjSdEy6UEfUCPcEUOfwQXacU6iGKJRkcN8pneTY-AhNAoAQVhD2PKAznxS4ED+QIPCgrZYPFQJ2lhAB6IioAAAWAZgAFo6EgdEaJHcQAHF8TQX8AAtDknYF1QNeFoBTAArFVKKgFNoQ4ARBljPiIAAOiUOdYgXURDTQNcYi3MM9UHEwkJ-KBT1zTdLzNG8klktBHwGF8WQgshP2-X8OH-AigOk4YwMseycJggIEOqL9kNQrh0Mw7CNlw9B8MIkjyMomjaDo4AGO4ZjWI46A0GzXN8yiaEvw4BAwEgcQPggCAwFYYBOMMhAeBRWSFKU097ByvMC3QDTN23HS9wVfSTzPEzAivG8syMjq5ms59lh8xzkL-aA3IGECRmwNIfMivy4ICwaULQiAMKCCKfB2mLYX2KBdIoAZPDsfZFAAcj8STgCegBudUkDeobjIvYI6nuqBHqgJ6BCUtBPsMlUYjYwzhoB2d5xQ7hyr+MlzMsmaGjmj9QxuyDP1Bl6ftPJ7Mle09noQJ6AIulqFzONGDQAZVhpgcmZrh0bkhxLGkOSrvIEWwap97-l87YwW53n+YQJRZbZjm0EFiG4eR5TvtoAJMZ3HQJrPPL0Bx2zmgmRRCYch7Qme8WKe+36wdp+ndkIxntfQdnIZyPwdbQPnHEF4XRdJt66dkbbpcUP30EDywlFjtBvbhtWVaUIA

Steps to reproduce

  1. Open the TypeScript Playground link.
  2. Notice that the useQueryLoose example incorrectly accepts the wrong UseQueryResult<{ wow: string }> annotation.
  3. Notice that the useQueryFenced example correctly rejects that wrong annotation, but brokenSecond?.second fails to type-check.
  4. Notice that the useQueryDistributed example still rejects the wrong annotation and allows the discriminated-union narrowing case.

Expected behavior

query.data?.type === 'first' ? undefined : query.data should narrow to { type: 'second'; second: string } | undefined, so second?.second should type-check.

At the same time, an incorrect contextual annotation like UseQueryResult<{ wow: string }> should still be rejected when the query options produce { wow: boolean }.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

  • TypeScript type-checking
  • Every runtime platform

Tanstack Query adapter

React Query

TanStack Query version

5.101.2

TypeScript version

5.9.3

Additional context

A possible fix direction is a distributive wrapper:

type NarrowableNoInfer<T> = T extends unknown ? NoInfer<T> : never

The playground shows this preserving the reverse-inference guard while restoring discriminated-union narrowing.

Related context:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions