Skip to content
Back to skills

Router Core Type Safety

ASecurity

Full type inference philosophy (never cast, never annotate inferred values), Register module declaration, from narrowing on hooks and Link, strict:false for shared components, getRouteApi for code-split typed access, addChildren with object syntax for TS perf, LinkProps and ValidateLinkOptions type utilities, as const satisfies pattern.

  • 8 stars
  • 0 votes
  • 0 copies
  • 6 views
  • Added September 11, 2026
developmenttypescriptgoreactnextjsnodeapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 11, 2026

npx -y skills add hashintel/brunch --skill router-core-type-safety --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Router Core Type Safety?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Router Core Type Safety
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hashintel-router-core-type-safety/badge)](https://www.skillsdirectory.com/skills/hashintel-router-core-type-safety)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: router-core-type-safety
description: >-
  Full type inference philosophy (never cast, never annotate inferred
  values), Register module declaration, from narrowing on hooks and
  Link, strict:false for shared components, getRouteApi for code-split
  typed access, addChildren with object syntax for TS perf, LinkProps
  and ValidateLinkOptions type utilities, as const satisfies pattern.
type: sub-skill
library: tanstack-router
library_version: '1.166.2'
requires:
  - router-core
sources:
  - TanStack/router:docs/router/guide/type-safety.md
  - TanStack/router:docs/router/guide/type-utilities.md
  - TanStack/router:docs/router/guide/render-optimizations.md
---

# Type Safety

TanStack Router is FULLY type-inferred. Params, search params, context, and loader data all flow through the route tree automatically. The **#1 AI agent mistake** is adding type annotations, casts, or generic parameters to values that are already inferred.

> **CRITICAL**: NEVER use `as Type`, explicit generic params, `satisfies` on hook returns, or type annotations on inferred values. Every cast masks real type errors and breaks the inference chain.
> **CRITICAL**: Do NOT confuse TanStack Router with Next.js or React Router. There is no `getServerSideProps`, no `useSearchParams()`, no `useLoaderData()` from `react-router-dom`.

## The ONE Required Type Annotation: Register

Without this, top-level exports like `Link`, `useNavigate`, `useSearch` have no type safety.

```tsx
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

const router = createRouter({ routeTree })

// THIS IS REQUIRED — the single type registration for the entire app
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

export default router
```

After registration, every `Link`, `useNavigate`, `useSearch`, `useParams` across the app is fully typed.

## Types Flow Automatically

### Route Hooks — No Annotation Needed

```tsx
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  validateSearch: (search: Record<string, unknown>) => ({
    page: Number(search.page ?? 1),
  }),
  loader: async ({ params }) => {
    // params.postId is already typed as string — do not annotate
    const post = await fetchPost(params.postId)
    return { post }
  },
  component: PostComponent,
})

function PostComponent() {
  // ALL of these are fully inferred — do NOT add type annotations
  const { postId } = Route.useParams()
  //      ^? string

  const { page } = Route.useSearch()
  //      ^? number

  const { post } = Route.useLoaderData()
  //      ^? { id: string; title: string; body: string }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>Page {page}</p>
    </div>
  )
}
```

### Context Flows Through the Tree

```tsx
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'

interface RouterContext {
  auth: { userId: string; role: 'admin' | 'user' } | null
}

// Note: createRootRouteWithContext is a FACTORY — call it TWICE: ()()
export const Route = createRootRouteWithContext<RouterContext>()({
  component: () => <Outlet />,
})
```

```tsx
// src/routes/dashboard.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/dashboard')({
  beforeLoad: ({ context }) => {
    // context.auth is already typed as { userId: string; role: 'admin' | 'user' } | null
    // NO annotation needed
    if (!context.auth) throw redirect({ to: '/login' })
    return { user: context.auth }
  },
  loader: ({ context }) => {
    // context.user is typed as { userId: string; role: 'admin' | 'user' }
    // This was added by beforeLoad above — fully inferred
    return fetchDashboard(context.user.userId)
  },
  component: DashboardComponent,
})

function DashboardComponent() {
  const data = Route.useLoaderData()
  const { user } = Route.useRouteContext()
  return <h1>Welcome {user.userId}</h1>
}
```

## Narrowing with `from`

Without `from`, hooks return a union of ALL routes' types — slow for TypeScript and imprecise.

### On Hooks

```tsx
import { useSearch, useParams, useNavigate } from '@tanstack/react-router'

function PostSidebar() {
  // WRONG — search is a union of ALL routes' search params
  const search = useSearch()

  // CORRECT — search is narrowed to /posts/$postId's search params
  const search = useSearch({ from: '/posts/$postId' })
  //    ^? { page: number }

  // CORRECT — params narrowed to this route
  const { postId } = useParams({ from: '/posts/$postId' })

  // CORRECT — navigate narrowed for relative paths
  const navigate = useNavigate({ from: '/posts/$postId' })
}
```

### On `Link`

```tsx
import { Link } from '@tanstack/react-router'

// WRONG — search resolves to union of ALL routes' search params, slow TS check
<Link to=".." search={{ page: 0 }} />

// CORRECT — narrowed, fast TS check
<Link from="/posts/$postId" to=".." search={{ page: 0 }} />

// Also correct — Route.fullPath in route components
<Link from={Route.fullPath} to=".." search={{ page: 0 }} />
```

## Shared Components: `strict: false`

When a component is used across multiple routes, use `strict: false` instead of `from`:

```tsx
import { useSearch } from '@tanstack/react-router'

function GlobalSearch() {
  // Returns union of all routes' search params — no runtime error if route doesn't match
  const search = useSearch({ strict: false })
  return <span>Query: {search.q ?? ''}</span>
}
```

## Code-Split Files: `getRouteApi`

Use `getRouteApi` instead of importing `Route` to avoid pulling route config into the lazy chunk:

```tsx
// src/routes/posts.lazy.tsx
import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router'

const routeApi = getRouteApi('/posts')

export const Route = createLazyFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  const data = routeApi.useLoaderData()
  const { page } = routeApi.useSearch()
  return <div>Page {page}</div>
}
```

## TypeScript Performance

### Use Object Syntax for `addChildren` in Large Route Trees

```tsx
// SLOWER — tuple syntax
const routeTree = rootRoute.addChildren([
  postsRoute.addChildren([postRoute, postsIndexRoute]),
  indexRoute,
])

// FASTER — object syntax (TS checks objects faster than large tuples)
const routeTree = rootRoute.addChildren({
  postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
  indexRoute,
})
```

With file-based routing the route tree is generated, so this is handled for you.

### Avoid Returning Unused Inferred Types from Loaders

When using external caches like TanStack Query, don't let the router infer complex return types you never consume:

```tsx
// SLOWER — TS infers the full ensureQueryData return type into the route tree
export const Route = createFileRoute('/posts/$postId')({
  loader: ({ context: { queryClient }, params: { postId } }) =>
    queryClient.ensureQueryData(postQueryOptions(postId)),
  component: PostComponent,
})

// FASTER — void return, inference stays out of the route tree
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ context: { queryClient }, params: { postId } }) => {
    await queryClient.ensureQueryData(postQueryOptions(postId))
  },
  component: PostComponent,
})
```

### `as const satisfies` for Link Option Objects

Never use `LinkProps` as a variable type — it's an enormous union:

```tsx
import type { LinkProps, RegisteredRouter } from '@tanstack/react-router'

// WRONG — LinkProps is a massive union, extremely slow TS check
const wrongProps: LinkProps = { to: '/posts' }

// CORRECT — infer a precise type, validate against LinkProps
const goodProps = { to: '/posts' } as const satisfies LinkProps

// EVEN BETTER — narrow LinkProps with generic params
const narrowedProps = {
  to: '/posts',
} as const satisfies LinkProps<RegisteredRouter, string, '/posts'>
```

### Type-Safe Link Option Arrays

```tsx
import type { LinkProps } from '@tanstack/react-router'

export const navLinks = [
  { to: '/posts' },
  { to: '/posts/$postId', params: { postId: '1' } },
] as const satisfies ReadonlyArray<LinkProps>

// Use the precise inferred type, not LinkProps directly
export type NavLink = (typeof navLinks)[number]
```

## Type Utilities for Generic Components

### `ValidateLinkOptions` — Type-Safe Link Props in Custom Components

```tsx
import {
  Link,
  type RegisteredRouter,
  type ValidateLinkOptions,
} from '@tanstack/react-router'

interface NavItemProps<
  TRouter extends RegisteredRouter = RegisteredRouter,
  TOptions = unknown,
> {
  label: string
  linkOptions: ValidateLinkOptions<TRouter, TOptions>
}

export function NavItem<TRouter extends RegisteredRouter, TOptions>(
  props: NavItemProps<TRouter, TOptions>,
): React.ReactNode
export function NavItem(props: NavItemProps): React.ReactNode {
  return (
    <li>
      <Link {...props.linkOptions}>{props.label}</Link>
    </li>
  )
}

// Usage — fully type-safe
<NavItem label="Posts" linkOptions={{ to: '/posts' }} />
<NavItem label="Post" linkOptions={{ to: '/posts/$postId', params: { postId: '1' } }} />
```

### `ValidateNavigateOptions` — Type-Safe Navigate in Utilities

```tsx
import {
  useNavigate,
  type RegisteredRouter,
  type ValidateNavigateOptions,
} from '@tanstack/react-router'

export function useDelayedNavigate<
  TRouter extends RegisteredRouter = RegisteredRouter,
  TOptions = unknown,
>(
  options: ValidateNavigateOptions<TRouter, TOptions>,
  delayMs: number,
): () => void
export function useDelayedNavigate(
  options: ValidateNavigateOptions,
  delayMs: number,
): () => void {
  const navigate = useNavigate()
  return () => {
    setTimeout(() => navigate(options), delayMs)
  }
}

// Usage — type-safe
const go = useDelayedNavigate(
  { to: '/posts/$postId', params: { postId: '1' } },
  500,
)
```

### `ValidateRedirectOptions` — Type-Safe Redirect in Utilities

```tsx
import {
  redirect,
  type RegisteredRouter,
  type ValidateRedirectOptions,
} from '@tanstack/react-router'

export async function fetchOrRedirect<
  TRouter extends RegisteredRouter = RegisteredRouter,
  TOptions = unknown,
>(
  url: string,
  redirectOptions: ValidateRedirectOptions<TRouter, TOptions>,
): Promise<unknown>
export async function fetchOrRedirect(
  url: string,
  redirectOptions: ValidateRedirectOptions,
): Promise<unknown> {
  const response = await fetch(url)
  if (!response.ok && response.status === 401) throw redirect(redirectOptions)
  return response.json()
}
```

### Render Props for Maximum Performance

Instead of accepting `LinkProps`, invert control so `Link` is narrowed at the call site:

```tsx
function Card(props: { title: string; renderLink: () => React.ReactNode }) {
  return (
    <div>
      <h2>{props.title}</h2>
      {props.renderLink()}
    </div>
  )
}

// Link narrowed to exactly /posts — no union check
;<Card title="All Posts" renderLink={() => <Link to="/posts">View</Link>} />
```

## Render Optimizations

### Fine-Grained Selectors with `select`

```tsx
function PostTitle() {
  // Only re-renders when page changes, not when other search params change
  const page = Route.useSearch({ select: ({ page }) => page })
  return <span>Page {page}</span>
}
```

### Structural Sharing

Preserve referential identity across re-renders for search params:

```tsx
const router = createRouter({
  routeTree,
  defaultStructuralSharing: true, // Enable globally
})

// Or per-hook
const result = Route.useSearch({
  select: (search) => ({ foo: search.foo, label: `Page ${search.foo}` }),
  structuralSharing: true,
})
```

Structural sharing only works with JSON-compatible data. TypeScript will error if you return class instances with `structuralSharing: true`.

## Common Mistakes

### 1. CRITICAL: Adding type annotations or casts to inferred values

```tsx
// WRONG — casting masks real type errors
const search = useSearch({ from: '/posts' }) as { page: number }

// WRONG — unnecessary annotation
const params: { postId: string } = useParams({ from: '/posts/$postId' })

// WRONG — generic param on hook
const data = useLoaderData<{ posts: Post[] }>({ from: '/posts' })

// CORRECT — let inference work
const search = useSearch({ from: '/posts' })
const params = useParams({ from: '/posts/$postId' })
const data = useLoaderData({ from: '/posts' })
```

### 2. HIGH: Using un-narrowed `LinkProps` type

```tsx
// WRONG — LinkProps is a massive union, causes severe TS slowdown
const myProps: LinkProps = { to: '/posts' }

// CORRECT — use as const satisfies for precise inference
const myProps = { to: '/posts' } as const satisfies LinkProps
```

### 3. HIGH: Not narrowing `Link`/`useNavigate` with `from`

```tsx
// WRONG — search is a union of ALL routes, TS check grows with route count
<Link to=".." search={{ page: 0 }} />

// CORRECT — narrowed, fast check
<Link from={Route.fullPath} to=".." search={{ page: 0 }} />
```

### 4. CRITICAL (cross-skill): Missing router type registration

```tsx
// WRONG — Link/useNavigate have no autocomplete, all paths are untyped strings
const router = createRouter({ routeTree })
// (no declare module)

// CORRECT — always register
const router = createRouter({ routeTree })
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}
```

### 5. CRITICAL (cross-skill): Generating Next.js or Remix patterns

```tsx
// WRONG — these are NOT TanStack Router APIs
export async function getServerSideProps() { ... }
export async function loader({ request }) { ... } // Remix-style
const [searchParams, setSearchParams] = useSearchParams() // React Router

// CORRECT — TanStack Router APIs
export const Route = createFileRoute('/posts')({
  loader: async () => { ... },           // TanStack loader
  validateSearch: zodValidator(schema),   // TanStack search validation
  component: PostsComponent,
})
const search = Route.useSearch()          // TanStack hook
```

See also: router-core (Register setup), router-core-navigation (from narrowing), router-core-code-splitting (getRouteApi).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in development →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.