#Type inference gap between Tanstack Start and Tanstack Query

1 messages · Page 1 of 1 (latest)

grim quail
#

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 />
    </>
  )
}

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?

kind summit
#

unless you return loader data I don't see a way

grim quail
#

gotcha, that would mean sending the payload twice, which isn't ideal as well.

Guess null assertions is the only way.

kind summit
#

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

grim quail
#

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;

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
}

do you see any issues with throwing an error in a RouteComponent? ^

kind summit
#

what do you expect to happen when throwing in the component ?

grim quail
kind summit
#

yes that's what should happen. there will be an unavoidable error logged to the browser console btw