T
TanStack•3mo ago
adverse-sapphire

Weird type bug when using `queryOptions` with `useQueries`

If you have a type where the only required key is name, the types of results in the combine function seems to be wrong. Any ideas on why this is?
import { queryOptions, useQueries } from "@tanstack/react-query";

type BrokenType = {
name: string;
};

const brokenOptions = queryOptions({
queryKey: ["broken"],
queryFn: () => new Promise<BrokenType>(() => {}),
});

type WorkingType = {
notName: string;
};

const workingOptions = queryOptions({
queryKey: ["working"],
queryFn: () => new Promise<WorkingType>(() => {}),
});

function useWorkingQueries() {
useQueries({
queries: [brokenOptions],
combine: (results) => {
return {
// result.data is incorrectly typed as `BrokenType` (not unioned with `undefined`)
data: results.map((result) => result.data),
// result.isPending is incorrectly typed as `false` (not `boolean`)
pending: results.some((result) => result.isPending),
};
},
});

useQueries({
queries: [workingOptions],
combine: (results) => {
return {
// result.data is correctly typed as `WorkingType | undefined`
data: results.map((result) => result.data),
// result.isPending is correctly typed as `boolean`, not just `false`
pending: results.some((result) => result.isPending),
};
},
});
}
import { queryOptions, useQueries } from "@tanstack/react-query";

type BrokenType = {
name: string;
};

const brokenOptions = queryOptions({
queryKey: ["broken"],
queryFn: () => new Promise<BrokenType>(() => {}),
});

type WorkingType = {
notName: string;
};

const workingOptions = queryOptions({
queryKey: ["working"],
queryFn: () => new Promise<WorkingType>(() => {}),
});

function useWorkingQueries() {
useQueries({
queries: [brokenOptions],
combine: (results) => {
return {
// result.data is incorrectly typed as `BrokenType` (not unioned with `undefined`)
data: results.map((result) => result.data),
// result.isPending is incorrectly typed as `false` (not `boolean`)
pending: results.some((result) => result.isPending),
};
},
});

useQueries({
queries: [workingOptions],
combine: (results) => {
return {
// result.data is correctly typed as `WorkingType | undefined`
data: results.map((result) => result.data),
// result.isPending is correctly typed as `boolean`, not just `false`
pending: results.some((result) => result.isPending),
};
},
});
}
6 Replies
like-gold
like-gold•3mo ago
I was interested in this, since it seemed like an absolutely hilarious issue, and so I went on a bit of a hunt. In the types for useQueries, there is a utility type called QueriesResults which is used to get the return type of useQueries, and this type (among several others in the file), reference a utility typeGetDefinedOrUndefinedQueryResult which uses the type of initialData to determine if the query has default data or not. The relevant start of this type is as follows:
T extends {
initialData?: infer TInitialData
}
? unknown extends TInitialData
? UseQueryResult<TData, TError>
: TInitialData extends TData
? DefinedUseQueryResult<TData, TError>
... // more below here, but this is the important step
T extends {
initialData?: infer TInitialData
}
? unknown extends TInitialData
? UseQueryResult<TData, TError>
: TInitialData extends TData
? DefinedUseQueryResult<TData, TError>
... // more below here, but this is the important step
Well, urns out that the type of our TInitialData in our broken example that @troywoy linked is BrokenType | InitialDataFunction<BrokenType>. This is because we are first checking if T extends initialData? (which will match every time that initialData does or more importantly doesn't exist on the object) Further investigation shows that InitialDataFunction is defined as the below type in querycore:
type InitialDataFunction<T> = () => T | undefined
type InitialDataFunction<T> = () => T | undefined
Well the thing is, () => T | undefined might look like a function, functions in javascript are actually objects, and by default, they have several properties on them, one of which is this: { name: string }. You can verify this by simply attempting to access a key on a function type and will see that several exist (although don't show visibly when you hover example normally):
type example = () => any
type name = example['name']
// ^? string
type example = () => any
type name = example['name']
// ^? string
So in conclusion, if your value is an object that is a subset of the base object that all functions in JavaScript extend, then the GetDefinedOrUndefinedQueryResult utility type is going to fail to get pass this check, because () => any does in fact extend { name: string }, and such, your type is DefinedQueryOptions. I'll update this post once I've thought of a good solution to that, and will see if I can make a PR tonight or tomorrow. but in the meantime, that was the problem for anyone interested
like-gold
like-gold•3mo ago
If anyone is interested, I have made a fix here: https://github.com/TanStack/query/pull/9278
GitHub
fix(react-query): update Incorrect Inference of GetDefinedOrUndefin...
Overview This PR resolves an issue that was brought up in discord where using queryOptions where TQueryFnData was { name: string }, would cause the type of TData on useQueries to be { name: string ...
exotic-emerald
exotic-emerald•3mo ago
NO way 🤪 What a great find
optimistic-gold
optimistic-gold•3mo ago
Nice. What you say makes total sense and what a quirk
adverse-sapphire
adverse-sapphireOP•3mo ago
Thanks for looking into this!

Did you find this page helpful?