Skip to content

fix(react-query): drop NoInfer from data source state type - #57

Merged
ArturAbdullin merged 1 commit into
mainfrom
fix/state-union-narrowing
Aug 7, 2026
Merged

fix(react-query): drop NoInfer from data source state type#57
ArturAbdullin merged 1 commit into
mainfrom
fix/state-union-narrowing

Conversation

@ArturAbdullin

Copy link
Copy Markdown
Contributor

Problem

If a data source's fetch returns a discriminated union, consumers cannot narrow
data down to a single branch. Any such data source is affected:

type Payload = {kind: 'empty'} | {kind: 'filled'; items: string[]};

const payloadDataSource = makePlainQueryDataSource({
    name: 'payload',
    fetch: (): Promise<Payload> => fetchPayload(),
});

Narrowing by the discriminant then fails in the most common way of writing it:

const {data} = useQueryData(payloadDataSource, {});
//     ^? NoInfer<Payload> | undefined   — expected Payload | undefined

const filled = data?.kind === 'filled' ? data : undefined;
filled?.items;
// TS2339: Property 'items' does not exist on type 'Payload'.
//   Property 'items' does not exist on type '{ kind: "empty"; }'.

This happens regardless of the discriminant's type (string or boolean), and
whether or not the data source declares transformResponse.

Cause

NoInfer was applied to the TState argument of PlainQueryDataSource /
InfiniteQueryDataSource — the data position of the state. NoInfer<T> is
implemented as a substitution type: transparent for assignability, but not a
union type, so discriminant narrowing does not work through it.

This is not an edge-case syntax issue — it breaks the whole "narrow, then store in
a variable" pattern: narrowing is discarded the moment the type is captured (a
const/let without an annotation, an array literal element, a generic call
argument).

NoInfer is not needed in TState: it is the phantom field
[stateHintSymbol]?: TState, never populated by the user, so nothing is ever
inferred from it.

Solution

NoInfer is removed only from the TState argument. It is kept in
TOptions, TFetchContext, next/prev, transformParams and tags, where
these are genuine inference positions. The change is type-level only and does not
touch runtime.

After the fix, data hovers as Payload | undefined and the narrowed filled
as {kind: 'filled'; items: string[]} | undefined.

Consumers now get narrowed types where narrowing previously failed silently. This
can surface as new compile errors where a narrowed value lands in a mutable slot
and another union branch is written to it later:

let current = data?.kind === 'filled' ? data : undefined;
current = {kind: 'empty'}; // now TS2322 — annotate as `Payload | undefined`

Read-only usage is unaffected: a narrower type is assignable everywhere the wider
one was accepted.

@ArturAbdullin
ArturAbdullin merged commit 1819349 into main Aug 7, 2026
3 checks passed
@ArturAbdullin
ArturAbdullin deleted the fix/state-union-narrowing branch August 7, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants