T
TanStack4w ago
genetic-orange

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
adverse-sapphire
adverse-sapphire4w ago
unless you return loader data I don't see a way
genetic-orange
genetic-orangeOP4w ago
gotcha, that would mean sending the payload twice, which isn't ideal as well. Guess null assertions is the only way.
adverse-sapphire
adverse-sapphire4w 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
genetic-orange
genetic-orangeOP4w 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? ^
adverse-sapphire
adverse-sapphire4w ago
what do you expect to happen when throwing in the component ?
genetic-orange
genetic-orangeOP4w 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.
adverse-sapphire
adverse-sapphire4w ago
yes that's what should happen. there will be an unavoidable error logged to the browser console btw
genetic-orange
genetic-orangeOP4w 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?