T
TanStack•15mo ago
equal-aqua

Getting the type for the "to" prop

Edit: Workaround I worked around this by explicitly defining the route options. I'm a bit puzzled why this works, since the exported NavigateOptions generic TRouter param defaults to RegisteredRouter, but somehow it doesn't get inferred in certain instances. Note that this is only necessary when using TS 5.5
import {
type NavigateOptions,
type RegisteredRouter,
} from '@tanstack/react-router'

export type NavigateTo = NavigateOptions<RegisteredRouter>['to']
import {
type NavigateOptions,
type RegisteredRouter,
} from '@tanstack/react-router'

export type NavigateTo = NavigateOptions<RegisteredRouter>['to']
___ At some point I could import { type LinkProps } and use LinkProps['to'] to get the type for all registered routes, so I could use this for helper functions that took a valid route. Now it stopped working and I'm not sure in what version (edit: Seems this is due to TypeScript 5.5, it works when downgrading to 5.4). Before I go down the version downgrade rabbit hole to figure out when it broke, what's the recommended way to reference this type? Also checked NavigateOptions['to'] to no avail.
10 Replies
equal-aqua
equal-aquaOP•15mo ago
Ah, we are using TS 5.5-beta-rc-1 which seems the be causing this. Switching VS Code to use 5.4 seems to correctly resolve the types.
xenial-black
xenial-black•15mo ago
this is tricky since LinkProps does a lot of TS infering that depends on the params being passed in what exactly does your "helper function" do?
rival-black
rival-black•15mo ago
On a project I'm using export type Routes = RoutePaths<typeof routeTree>; to have a type I can use in a wrapper of the Link component
xenial-black
xenial-black•15mo ago
for that Link wrapper use case we have a yet undocumented function called createLink which reminds me we should document this ...
rival-black
rival-black•15mo ago
Back to the original question, one more usecase I had for the type holding all routes was something like a Map<Route, UserGroup[]> to centralize authorization config on some pages (and show/hide links in the navbar)
equal-aqua
equal-aquaOP•15mo ago
what exactly does your "helper function" do? @Manuel Schiller
It's for guard functions that checks for a certain condition and takes a redirection URL as argument, pseudo examples:
authenticatedOr('/login')
hasPermissionOr('create_post', '/posts')
hasAccountPlanOr('pro', '/upgrade')

function authenticatedOr(redirectTo: NavigationProps['to']) {
if (!isAuthenticated()) {
throw redirect({ to: redirectTo, replace: true })
}
}
authenticatedOr('/login')
hasPermissionOr('create_post', '/posts')
hasAccountPlanOr('pro', '/upgrade')

function authenticatedOr(redirectTo: NavigationProps['to']) {
if (!isAuthenticated()) {
throw redirect({ to: redirectTo, replace: true })
}
}
We want to enjoy the full typing for the redirectTo param.
xenial-black
xenial-black•15mo ago
how would you handle search params and path params?
equal-aqua
equal-aquaOP•15mo ago
The URLs we redirect to are generally paramless, so it's not really an issue. But if they are needed we'll simply add the bare minimum to the helper signature
xenial-black
xenial-black•15mo ago
so in essence you need the same signature as redirect
equal-aqua
equal-aquaOP•15mo ago
Perhaps, not yet needed all of that, I think I want to keep it super simple and readble, for my usecase it's mainly been passing a to string. But maybe I should get used to getting the redirect signature:
// Current clean API:
authenticatedOr('/login')
hasPermissionOr('create_post', '/posts')
hasAccountPlanOr('pro', '/upgrade')

// Using redirect signature:
authenticatedOr({ to: '/login' })
loggedOutOr({ to: '/accounts/$id', params: { id: accountId } })
hasAccountPlanOr('pro', { to: '/upgrade' })
// Current clean API:
authenticatedOr('/login')
hasPermissionOr('create_post', '/posts')
hasAccountPlanOr('pro', '/upgrade')

// Using redirect signature:
authenticatedOr({ to: '/login' })
loggedOutOr({ to: '/accounts/$id', params: { id: accountId } })
hasAccountPlanOr('pro', { to: '/upgrade' })
Becomes a bit more verbose, then you could almost argue to take out the redirect logic 😅 but definitely not as clean as our first take.
if (!authenticated()) throw redirect({ to: '/login' })

if (!loggedOut()) throw redirect({ to: '/accounts/$id', params: { id: accountId } })

if (!hasAccountPlan('pro') throw redirect({ to: '/upgrade' })
if (!authenticated()) throw redirect({ to: '/login' })

if (!loggedOut()) throw redirect({ to: '/accounts/$id', params: { id: accountId } })

if (!hasAccountPlan('pro') throw redirect({ to: '/upgrade' })
I worked around this by explicitly defining the route options. I'm a bit puzzled why this works, since the exported NavigateOptions generic TRouter param defaults to RegisteredRouter, but somehow it doesn't get inferred in certain instances.
import {
type NavigateOptions,
type RegisteredRouter,
} from '@tanstack/react-router'

export type NavigateTo = NavigateOptions<RegisteredRouter>['to']
import {
type NavigateOptions,
type RegisteredRouter,
} from '@tanstack/react-router'

export type NavigateTo = NavigateOptions<RegisteredRouter>['to']

Did you find this page helpful?