T
TanStack3w ago
rare-sapphire

Type inference gap between Tanstack Start and Tanstack Query

When a TanStack Start loader redirects based on query data (like redirecting if user is null), the runtime guarantees the route will never render in that state. But TypeScript doesn’t pick that up — it still infers user as User | null in the component.
export const Route = createFileRoute('/(app)')({
component: RouteComponent,

async loader({ context }) {
const { queryClient } = context

const { user } = await queryClient.ensureQueryData(
authUserQueryOptions(),
)

if (!user) {
throw redirect({
to: '/auth/login'
})
}
},
})

function RouteComponent() {
const {
data: { user }, // <-- user can still be undefined
} = useSuspenseQuery(authUserQueryOptions())

return (
<>
<Header user={user} />
<Outlet />
<AsideNav />
</>
)
}
export const Route = createFileRoute('/(app)')({
component: RouteComponent,

async loader({ context }) {
const { queryClient } = context

const { user } = await queryClient.ensureQueryData(
authUserQueryOptions(),
)

if (!user) {
throw redirect({
to: '/auth/login'
})
}
},
})

function RouteComponent() {
const {
data: { user }, // <-- user can still be undefined
} = useSuspenseQuery(authUserQueryOptions())

return (
<>
<Header user={user} />
<Outlet />
<AsideNav />
</>
)
}
Problem The loader guarantees that user isn’t null, but TypeScript doesn’t know that — so you have to use a null assertion or redundant check. Question Is there a recommended way to tell TypeScript that the redirect ensures non-null data for this route without null assertions?
8 Replies
ratty-blush
ratty-blush3w ago
unless you return loader data I don't see a way
rare-sapphire
rare-sapphireOP3w ago
gotcha, that would mean sending the payload twice, which isn't ideal as well. Guess null assertions is the only way.
ratty-blush
ratty-blush3w ago
it wouldn't mean that as our serializer uses reference sharing but the problem is rather that when consuming the loader data without a query hook no query observer is subscribed
rare-sapphire
rare-sapphireOP3w ago
You are right, I just checked the html output, it does seem like only one set of data is included, not sure why I thought it would be loader data + query data. Regarding query observer, one easy 'fix' is to use fallback value. i.e.
const routeData = Route.useLoaderData();
const queryData = useSuspenseQuery(queryOptions());

const user = queryData ?? routeData;
const routeData = Route.useLoaderData();
const queryData = useSuspenseQuery(queryOptions());

const user = queryData ?? routeData;
routeData will please typescript, since it's protected in the runtime by loader. Right now I'm doing something like this though, i.e. creating a wrapper hook, and throw an error.
export function getUserQueryOptions() {
return queryOptions({ queryKey: ['auth-user'], queryFn: () => getUser() })
}

export function useAuthUser() {
const { data } = useSuspenseQuery(getUserQueryOptions())

if (data.type !== 'SUCCESS') {
throw new Error('useAuthUser must be used within a protected route')
}

return data.value
}
export function getUserQueryOptions() {
return queryOptions({ queryKey: ['auth-user'], queryFn: () => getUser() })
}

export function useAuthUser() {
const { data } = useSuspenseQuery(getUserQueryOptions())

if (data.type !== 'SUCCESS') {
throw new Error('useAuthUser must be used within a protected route')
}

return data.value
}
do you see any issues with throwing an error in a RouteComponent? ^
ratty-blush
ratty-blush3w ago
what do you expect to happen when throwing in the component ?
rare-sapphire
rare-sapphireOP3w ago
technically it shouldn't happen, as it's protected by the loader. I'm just wondering if there will be any unexpected behaviour. Currently it renders the errorComponent when I throw the error, which seems right.
ratty-blush
ratty-blush3w ago
yes that's what should happen. there will be an unavoidable error logged to the browser console btw
rare-sapphire
rare-sapphireOP3w ago
gotcha, thanks for the heads up. Though I don't expect it to happen due to runtime guarantees by the loader. Thanks!

Did you find this page helpful?