Summary
A custom base query in RTK Query that handles token refresh was added without correctly typing the return value of the inner refresh request. This caused TypeScript to fall back to unknown, leading to runtime errors and confusing editor tooling.
Root Cause
- The
refreshResultvariable was typed asanybecause the genericBaseQueryFnwas instantiated withunknownfor the success type. - The inner call to
baseQuerydoes not inherit the response schema of the refresh endpoint, so TypeScript cannot inferRefreshTokenResponse. - The mutation definition later in the slice also lacks an explicit
transformResponsetype, so the mismatch propagates.
Why This Happens in Real Systems
- Generic base queries often use
unknownfor simplicity, but when they wrap other queries they must propagate the concrete response type. - Engineers copy‑paste boilerplate and forget to override the generic parameters for each specialized use case.
- In large codebases, the type‑holes are hidden by
anycasts (as RefreshTokenResponse) that silence the compiler.
Real-World Impact
- Runtime crashes when
refreshResult.datais accessed on an object that doesn’t match the expected shape. - IDE autocompletion stops working, increasing development friction.
- Unit tests that mock the refresh endpoint fail silently, masking bugs until production.
Example or Code (if necessary and relevant)
import { BaseQueryFn, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react';
import type { RefreshTokenResponse, IApiResponse } from './types';
import { setNewAccessToken, logOut } from '../slices/authSlice';
import type { RootState } from '../store';
type RefreshResult = IApiResponse;
const baseQuery = fetchBaseQuery({ baseUrl: '/api' });
const baseQueryWithReauth: BaseQueryFn Propagate the concrete success type for the inner refresh call
RefreshResult
> = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions);
const refreshToken = (api.getState() as RootState).auth.refreshToken;
if (result.error?.status === 401 && refreshToken) {
const refreshResult = await baseQuery(
{
url: '/auth/refresh',
method: 'POST',
body: { refreshToken },
},
api,
extraOptions,
) as { data?: RefreshResult };
if (refreshResult?.data) {
api.dispatch(setNewAccessToken(refreshResult.data as RefreshTokenResponse));
result = await baseQuery(args, api, extraOptions);
} else {
console.error('Failed to fetch new access token:', refreshResult?.error);
api.dispatch(logOut());
}
}
return result;
};
How Senior Engineers Fix It
- Explicitly type the generic parameters of
BaseQueryFnto include the expected success schema (RefreshResult). - Use type‑guards or
isRejectWithValuechecks instead of rawascasts. - Centralize the refresh logic in a utility that returns a typed
Promise<RefreshResult>and reuse it. - Add unit tests that mock both successful and failed refresh flows, asserting the inferred types.
- Enable
noImplicitAnyandstrictNullChecksintsconfig.jsonto catch missing typings early.
Why Juniors Miss It
- They rely on implicit
anyand assume TypeScript will “just work”. - Lack of familiarity with generic overloads in RTK Query’s
BaseQueryFn. - Tendency to quick‑fix with
ascasts, which silences the compiler but hides the real problem. - Often focus on getting the feature to run rather than maintaining type safety across nested async calls.