type-safety

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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
.
TanStack Router 采用全类型推断设计。路径参数、搜索参数、上下文和 loader 数据都会自动在路由树中流转。AI Agent 最常犯的错误就是给已经可被推断的值添加类型注解、类型断言或泛型参数。
关键警告:绝不要对 hook 返回值使用
as Type
、显式泛型参数、
satisfies
,也不要给已推断值添加类型注解。每一次类型断言都会掩盖真实的类型错误,并破坏类型推断链。 关键警告:不要将 TanStack Router 与 Next.js 或 React Router 混淆。TanStack Router 没有
getServerSideProps
,没有
useSearchParams()
,也没有来自
react-router-dom
的
useLoaderData()
。

The ONE Required Type Annotation: Register

唯一必需的类型注解: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.
如果没有这个步骤,
Link
、
useNavigate
、
useSearch
等顶层导出将不具备类型安全性。
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
注册完成后,应用中所有的
Link
、
useNavigate
、
useSearch
、
useParams
都会具备完整的类型支持。

Types Flow Automatically

类型自动流转

Route Hooks — No Annotation Needed

路由 Hooks — 无需添加类型注解

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

使用
from
进行类型收窄

Without
from
, hooks return a union of ALL routes' types — slow for TypeScript and imprecise.
如果不使用
from
,hooks 会返回所有路由类型的联合类型——这会降低 TypeScript 的编译速度,且类型不够精确。

On Hooks

在 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' })
}
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

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

共享组件:
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>
}
当组件在多个路由中复用时,使用
strict: false
替代
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

代码分割文件:
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>
}
使用
getRouteApi
替代直接导入
Route
,避免将路由配置打包进懒加载 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

TypeScript 性能优化

Use Object Syntax for
addChildren
in Large Route Trees

大型路由树中为
addChildren
使用对象语法

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.
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,
})
如果使用文件路由,路由树是自动生成的,因此这一点已经被自动处理了。

Avoid Returning Unused Inferred Types from Loaders

避免从 loader 返回未使用的推断类型

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,
})
当使用 TanStack Query 等外部缓存时,不要让路由推断你根本不会用到的复杂返回类型:
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

为 Link 选项对象使用
as const satisfies

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'>
绝不要将
LinkProps
作为变量的类型——它是一个非常庞大的联合类型:
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

类型安全的 Link 选项数组

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]
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

ValidateLinkOptions
— 自定义组件中类型安全的 Link Props

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' } }} />
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
and
ValidateRedirectOptions

ValidateNavigateOptions
与
ValidateRedirectOptions

Same pattern as
ValidateLinkOptions
above, for
useNavigate
and
redirect
. Declare a generic public overload plus a non-generic implementation signature so the call site stays narrowed and the body works without casts:
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,
) {
  const navigate = useNavigate()
  return () => {
    setTimeout(() => navigate(options), delayMs)
  }
}
ValidateRedirectOptions
works identically — declare a generic overload accepting
ValidateRedirectOptions<TRouter, TOptions>
and an impl signature accepting
ValidateRedirectOptions
, then call
redirect(options)
in the body.
与上面的
ValidateLinkOptions
模式相同,用于
useNavigate
和
redirect
。声明一个泛型的公共重载签名,再加上一个非泛型的实现签名,这样调用处的类型会保持收窄状态,函数体内部也不需要类型断言:
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,
) {
  const navigate = useNavigate()
  return () => {
    setTimeout(() => navigate(options), delayMs)
  }
}
ValidateRedirectOptions
的用法完全相同——声明一个接受
ValidateRedirectOptions<TRouter, TOptions>
的泛型重载,以及一个接受
ValidateRedirectOptions
的实现签名,然后在函数体中调用
redirect(options)
即可。

Render Props for Maximum Performance

使用 Render Props 获得最佳性能

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>} />
与其直接接受
LinkProps
,不如反转控制流,让
Link
在调用处完成类型收窄:
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

使用
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>
}
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
.
在多次渲染间保持搜索参数的引用一致性:
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,
})
结构共享仅适用于 JSON 兼容的数据。如果你在开启
structuralSharing: true
的情况下返回类实例,TypeScript 会报错。

Common Mistakes

常见错误

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

1. 关键警告:给已推断值添加类型注解或类型断言

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

2. 高优先级:使用未收窄的
LinkProps
类型

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
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

3. 高优先级:未使用
from
收窄
Link
/
useNavigate
的类型

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

4. 关键警告(跨技能点):缺少路由类型注册

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
  }
}
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): Wrong-framework imports and file structure

5. 关键警告(跨技能点):导入了错误框架的 API 或使用了错误的文件结构

Wrong-framework code looks plausible (it's React) but breaks the build or produces conflicting
/
routes at runtime.
tsx
// WRONG — react-router-dom and next/* are different libraries
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import Link from 'next/link'
import { useRouter, useParams } from 'next/navigation'

// CORRECT — all routing exports come from @tanstack/react-router
import {
  Link,
  Outlet,
  useNavigate,
  useRouter,
  useLocation,
  useParams,
  redirect,
} from '@tanstack/react-router'
tsx
// WRONG file structures + APIs:
//   src/pages/*.tsx with getServerSideProps / getStaticProps  (Next.js Pages Router)
//   app/layout.tsx + app/page.tsx                              (Next.js App Router)
//   _app/index.tsx, pages/_app.tsx, pages/_document.tsx        (Next.js custom App)
//   loader/action exports                                       (Remix)

// CORRECT — TanStack file-based routing at src/routes/*.tsx
export const Route = createFileRoute('/posts')({
  loader: async () => { ... },
  validateSearch: zodValidator(schema),
  component: PostsComponent,
})
const search = Route.useSearch()
If a build error mentions
react-router-dom
,
next/
,
pages/_app
, or duplicate
/
routes, fix the import — don't paper over with type assertions.
错误框架的代码看起来似乎没问题(都是 React 语法),但会导致构建失败,或者在运行时产生冲突的
/
路由。
tsx
// WRONG — react-router-dom and next/* are different libraries
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import Link from 'next/link'
import { useRouter, useParams } from 'next/navigation'

// CORRECT — all routing exports come from @tanstack/react-router
import {
  Link,
  Outlet,
  useNavigate,
  useRouter,
  useLocation,
  useParams,
  redirect,
} from '@tanstack/react-router'
tsx
// WRONG file structures + APIs:
//   src/pages/*.tsx with getServerSideProps / getStaticProps  (Next.js Pages Router)
//   app/layout.tsx + app/page.tsx                              (Next.js App Router)
//   _app/index.tsx, pages/_app.tsx, pages/_document.tsx        (Next.js custom App)
//   loader/action exports                                       (Remix)

// CORRECT — TanStack file-based routing at src/routes/*.tsx
export const Route = createFileRoute('/posts')({
  loader: async () => { ... },
  validateSearch: zodValidator(schema),
  component: PostsComponent,
})
const search = Route.useSearch()
如果构建错误提到了
react-router-dom
、
next/
、
pages/_app
或重复的
/
路由,请修复导入问题——不要用类型断言来掩盖问题。

6. CRITICAL: Treating typecheck as proof of runtime schema propagation

6. 关键警告:将类型检查等同于运行时 schema 传递的证明

Types can say a field exists while a database projection, API serializer, or server function omits it. When adding or renaming a field, trace the value through storage, validation, handler output, loader data, and rendered UI. Do not cast the response to the desired type. Add a runtime assertion against the real handler or serialized response, such as
expect(await getOrder({ data: { id } })).toMatchObject({ totalCents: 2599 })
. Then run the route-level test and production build. The type test remains necessary, but it is not the runtime contract test.
See also: router-core (Register setup), router-core/navigation (from narrowing), router-core/code-splitting (getRouteApi).
类型系统可能认为某个字段存在,但数据库投影、API 序列化器或服务端函数可能已经将其省略。当添加或重命名字段时,请沿着存储、校验、处理器输出、loader 数据、渲染 UI 的完整链路追踪该值。不要将响应强制断言为你想要的类型。 添加针对真实处理器或序列化后响应的运行时断言,例如
expect(await getOrder({ data: { id } })).toMatchObject({ totalCents: 2599 })
。 然后运行路由级测试和生产构建。类型测试仍然必要,但它不能替代运行时契约测试。
另请参阅:router-core(Register 配置)、router-core/navigation(from 类型收窄)、router-core/code-splitting(getRouteApi)。