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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions