I am fetching some data in React with useQuery from tanstack. This hook returns UseQueryResult<TData, TError> and tanstack nicely infers TData from the queryFn making the fetch, but I can't make Typescript infer TError from the same function.
For more context:
Any errors returned from our backend respect the problem details spec, so the http client in the frontend parses api responses and returns IApiResponse with a valid IProblemDetails interface when a request fails.
export type IApiResponse<T> = BaseResponse &
(
| {
isError: false;
data: T;
}
| {
isError: true;
problem: IProblemDetails;
rawBody: unknown;
}
);
So I know the folowing function would either return data of T or IProblemDetails
export async function getData({resourceId}: {resourceId: string}) {
const res = await new ApiWithAuth().get<T>(
`api/resource/${resourceId}`
);
if (res.isError) {
return Promise.reject(res.problem);
}
return res.data;
}
For a query like this
const playlistQuery = useQuery(['get-user-playlist-by-id'], async () => {
const res = await getUserPlaylistById({ userId: userId!, playlistId });
if (res.isError) {
throw res.problem;
}
return res.data;
});
The result is typed like playlistQuery: UseQueryResult<UserPlaylist, unknown>
; it correctly knows the data returned, but not the error type when it fails. Both me and Typescript know that problem is IProblemDetails
when I throw res.problem
so why can't useQuery infer TError from there?
From these docs I understand UseQueryResult.error
is populated and typed based on the promise rejected/error thrown in the queryFn call so I would expect playlistQuery: IProblemDetails
to be inferred like TData is inferred to be UserPlaylist
I looked at this question and this blog , but I am not using an error boundry or triggering a side effect with onError
. Instead, different components of my page need to react locally to an error state, so I want a way for Typescript to know that when playlistQuery.isError
then playlistQuery.error
will hold an IProblemDetails
object.
How to achieve this without passing generics to useQuery or asserting the type? (both would be cumbersome and not typesafe)
TypeScript does know problem
is IProblemDetails
, but reject
accepts any
argument:
reject<T = never>(reason?: any): Promise<T>;
As explained nicely here, anything could happen inside your function in Javascript.
Even if you didn't throw any error, system exceptions like RangeError: Maximum call stack size exceeded
could happen. There is no guarantee to get an exception of a certain type.
Therefore, it's impossible to know this during compilation, and it's also what TypeScript does as default (useUnknownInCatchVariables):
try {
throw new AxiosError();
} catch (error: AxiosError) { // ❌ TS1196: type must be any or unknown if specified.
console.log(error.status)
}
The correct way is to narrow the type at runtime:
try {
throw new AxiosError();
} catch (error) { // error: unknown
if (isAxiosError(error)) { // ✅ Correct way
console.log(error.status)
}
}
There was a trick in v4 of react-query
to set TError
without setting TData
explicitly in useQuery
, using the onError
method:
export const useMyQuery = (payload: IPayload) => {
const { data, error} = useQuery({
queryKey: ['test'],
queryFn: () => API.fetchMyData(payload),
// This does the trick
onError: (err: IApiError) => err,
});
};
But unfortunately, it was removed in v5.
Right now only useMutation
have this property in v5.