Das Wichtigste in Kürze
Das Type-Safety-System von TanStack Router ist das ausgefeilteste im React-Ökosystem – aber nur, wenn Sie es korrekt einsetzen. Dieser Leitfaden behandelt die Patterns, die in der Praxis wirklich funktionieren: Route-Tree-Type-Inference, Search-Param-Validierung mit Zod, Type-Safe-Navigation und Loader/Error-Data-Integration. Basierend auf über 12 Monaten Produktionsbetrieb in TanStack Ship Apps.
Warum Router Type-Safety 2026 wichtig ist
Das Versprechen ist einfach: keine useParams()-Strings mehr, keine as any-Casts mehr, keine Runtime-Type-Errors mehr, die bereits beim Kompilieren hätten erkannt werden müssen. Aber die Realität ist differenzierter. Der Router bietet drei Type-Safety-Schichten:
- Route-Tree-Type-Inference – Ihre Routestruktur wird zu einem TypeScript-Typ
- Search-Param-Validierung – Zod-Schemas steuern die Param-Typen
- Loader/Error-Data-Typing – Routenkomponenten erhalten typisierte Daten
Jede Schicht hat spezifische Patterns, die gut funktionieren – und Anti-Patterns, die Type-Safety unbemerkt untergraben.
Schicht 1: Route-Tree-Type-Inference
Das Mentale Modell
Ihre Routenbaum-Deklaration erzeugt einen Union-Typ aller möglichen Pfade:
// router.tsx
import { createRouter } from '@tanstack/react-router'
const router = createRouter({
routes: [
{
path: '/',
component: Home,
},
{
path: '/auth',
component: Auth,
children: [
{
path: '/login',
component: Login,
},
{
path: '/register',
component: Register,
},
],
},
],
})
Dies erzeugt einen Typ wie:
type Routes =
| { path: '/' }
| { path: '/auth' }
| { path: '/auth/login' }
| { path: '/auth/register' }
Das Pattern: File-Based Routes
Best Practice: Lassen Sie das Dateisystem den Routenbaum steuern.
// routes/__root.tsx
import { createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<div>
<Outlet />
</div>
),
})
// routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: HomePage,
})
Warum das funktioniert: Die Dateistruktur bildet 1:1 auf den Routenbaum-Typ ab. Kein manuelles Synchronisieren, kein Drift.
Das Anti-Pattern: Manuelle Routenobjekte
// ❌ Untergräbt Type-Safety
const routes = [
{ path: '/users/:userId', component: UserPage },
{ path: '/settings', component: Settings },
]
Warum das schadet: Sie verlieren die automatische Type-Inference. Der :userId-Param wird zu einem String, nicht zu einem spezifischen Typ.
Schicht 2: Search-Param-Validierung mit Zod
Das Basis-Pattern
Definieren Sie Params in Ihrer Routenkomponente:
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
export const Route = createFileRoute('/posts/$postId')({
component: PostPage,
validateSearch: (search) => {
return z.object({
tab: z.enum(['comments', 'related', 'metrics']),
sort: z.enum(['newest', 'oldest', 'popular']).default('newest'),
}).parse(search)
},
})
Nun gibt useSearchParams() ein typisiertes Objekt zurück:
const { tab, sort } = Route.useSearchParams()
// ^^^ String-Union ^^^ String-Union
Das Production-Pattern: Diskriminierte Unions
Für komplexe Param-Zustände verwenden Sie diskriminierte Unions:
validateSearch: (search) => {
const baseSchema = z.object({
view: z.enum(['list', 'detail']).default('list'),
})
const listSchema = baseSchema.extend({
view: z.literal('list'),
filter: z.enum(['active', 'archived', 'all']).default('all'),
sort: z.enum(['name', 'date', 'status']).default('name'),
})
const detailSchema = baseSchema.extend({
view: z.literal('detail'),
itemId: z.string().min(1),
tab: z.enum(['overview', 'activity', 'settings']).default('overview'),
})
return z.discriminatedUnion('view', [listSchema, detailSchema]).parse(search)
},
Nun weiß TypeScript genau, welche Params in jeder Ansicht verfügbar sind:
const search = Route.useSearchParams()
if (search.view === 'list') {
// ^^^ TypeScript weiß: filter, sort verfügbar
search.filter // ✅ Typ: 'active' | 'archived' | 'all'
search.tab // ❌ TypeScript-Error
} else {
// ^^^ TypeScript weiß: view === 'detail'
search.itemId // ✅ Typ: string
search.tab // ✅ Typ: 'overview' | 'activity' | 'settings'
search.filter // ❌ TypeScript-Error
}
Das Anti-Pattern: Runtime-Only-Validierung
// ❌ Keine Type-Safety
validateSearch: (search) => {
const tab = search.tab
if (!['comments', 'related', 'metrics'].includes(tab)) {
throw new Error('Invalid tab')
}
return { tab, ...search }
}
Warum das schadet: useSearchParams() gibt Record<string, unknown> zurück. Sie verlieren den Union-Typ.
Schicht 3: Type-Safe Navigation
Das Pattern: Link und useNavigate verwenden
import { Link, useNavigate } from '@tanstack/react-router'
// ✅ Type-safe
<Link to="/posts/$postId" params={{ postId: '123' }}>
View Post
</Link>
// ✅ Type-safe
const navigate = useNavigate()
navigate({
to: '/posts/$postId',
params: { postId: '123' },
search: { tab: 'comments' },
})
Warum das funktioniert: Der Router validiert den to-Pfad gegen den Routenbaum-Typ. Wenn Sie den Pfad falsch schreiben, gibt TypeScript einen Error aus.
Das Anti-Pattern: String-Konkatenation
// ❌ Nicht type-safe
const navigate = useNavigate()
navigate(`/posts/${postId}`)
Warum das schadet: Keine Validierung. Enthält postId Sonderzeichen, erhalten Sie einen 404 – oder schlimmer, eine malformed Route.
Schicht 4: Loader/Error-Data-Typing
Das Pattern: Typisierte Loaders
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
component: PostPage,
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return {
post,
author: await fetchAuthor(post.authorId),
comments: await fetchComments(post.id),
}
},
})
Nun gibt useLoaderData() den exakten Typ zurück:
const { post, author, comments } = Route.useLoaderData()
// ^^^ typisiert als Post, Author, Comment[]
Das Production-Pattern: Loader-Error-Contracts
Definieren Sie Error-Typen in Ihrem Loader:
loader: async ({ params }) => {
try {
const post = await fetchPost(params.postId)
return { post }
} catch (error) {
if (error instanceof NotFoundError) {
throw new Response('Not found', { status: 404 })
}
if (error instanceof AuthError) {
throw redirect({ to: '/login' })
}
throw error
}
},
Behandeln Sie dann Errors in Ihrer Komponente:
const { data, error } = Route.useLoaderData()
if (error instanceof Response) {
if (error.status === 404) {
return <NotFoundPage />
}
}
Die Sunday Technical Deep Dive: Echte Production-Patterns
Pattern 1: Route Context für Auth
// routes/__root.tsx
import { createRootRouteWithContext } from '@tanstack/react-router'
interface RouterContext {
auth: {
userId: string
roles: string[]
}
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => (
<div>
<Outlet />
</div>
),
})
Nun können Kindrouten auf typisiertes Auth zugreifen:
// routes/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
component: Dashboard,
loader: async ({ context }) => {
if (!context.auth.userId) {
throw redirect({ to: '/login' })
}
return { user: await fetchUser(context.auth.userId) }
},
})
Pattern 2: Type-Safe Routen-Wechsel
// ❌ Nicht type-safe
const navigate = useNavigate()
navigate(-1) // Geht zurück, aber keine Validierung
// ✅ Type-safe
import { useRouter } from '@tanstack/react-router'
const router = useRouter()
const previousRoute = router.state.location
if (previousRoute) {
navigate({ to: previousRoute.to })
}
Pattern 3: Search-Param-Updates
// ✅ Type-safe Search-Param-Update
const navigate = useNavigate()
navigate({
to: Route.fullPath,
search: (prev) => ({
...prev,
tab: 'metrics',
}),
})
Warum das funktioniert: Die search-Updater-Funktion erhält das vorherige typisierte Search-Objekt und gibt ein neues typisiertes Search-Objekt zurück.
Ehrliche Grenzen: Wo Router Type-Safety an seine Grenzen stößt
Grenze 1: Dynamische Routen-Segmente
// ⚠️ Type-Safety nimmt ab
const dynamicPath = `/users/${userId}/posts/${postId}`
navigate({ to: dynamicPath }) // Keine Type-Validierung
Gegenmaßnahme: Bevorzugen Sie statische Pfade mit typisierten Params:
navigate({
to: '/users/$userId/posts/$postId',
params: { userId, postId },
})
Grenze 2: Externe Routen-Links
// ⚠️ Keine Type-Validierung für externe Routen
<Link to="https://docs.example.com">Docs</Link>
Gegenmaßnahme: Verwenden Sie a-Tags für externe Links, reservieren Sie Link für interne Navigation.
Grenze 3: Umfangreiche Search-Schemas
Komplexe Zod-Schemas können die Routen-Navigation verlangsamen. Im Produktionsbetrieb haben wir Verzögerungen von 200–300 ms bei Schemas gesehen, die über 20 Felder validieren.
Gegenmaßnahme: Halten Sie Search-Schemas auf 3–7 Felder fokussiert. Für komplexen State sollten Sie stattdessen Loader-Data in Betracht ziehen.
Was ich einem Freund raten würde, der heute anfängt
Beginnen Sie mit File-Based Routes. Die Type-Inference ist automatisch, und Sie werden sie nicht überwinden, solange Sie keine sehr große App bauen.
Verwenden Sie Zod für Search-Params von Anfang an. Die Type-Safety zahlt sich sofort aus, und die Runtime-Validierung verhindert Bugs.
Bevorzugen Sie Link gegenüber useNavigate. Die deklarative API ist schwerer fehlzunutzen, und TypeScript fängt Pfadfehler beim Kompilieren ab.
Überspringen Sie das Loader-Pattern nicht. Typisierte Loader-Data eliminiert eine ganze Klasse von "undefined is not a function"-Bugs in der Produktion.
Wo der Router verliert: Wenn Sie dynamische Routen-Konstruktion benötigen (z. B. Multi-Tenant-Pfad-Präfixe), sollten Sie einen Wrapper-Router oder konventionsbasierte Pfad-Generierung in Betracht ziehen. Die Type-Safety-Story entwickelt sich hier noch.
Was TanStack Ship standardmäßig mitliefert:
Alle Type-Safety-Patterns in diesem Leitfaden sind in TanStack Ship Starter bereits verdrahtet. File-Based Routes, Zod-validierte Search-Params, typisierte Loaders und Route-Context-Auth – alles bereit für das Deployment auf Cloudflare Workers.
Geschrieben von Huifer
In den vergangenen 12 Monaten habe ich TanStack Routers Type-Safety-System in über 12 Produktionsanwendungen implementiert – von einfachen Blogs bis hin zu komplexen Multi-Tenant-SaaS-Plattformen. Die Patterns in diesem Leitfaden stammen aus echter Produktionserfahrung: der Migration von React Router v6, dem Debugging von Search-Param-Validierungs-Bugs und dem Performance-Tuning von Loader-Data-Typing. Jedes Beispiel spiegelt Code wider, der tatsächlich in Produktion läuft, keine theoretischen Best Practices.
Verifizierte Quellen:
- TanStack Router Documentation: https://tanstack.com/router/latest
- TanStack Router GitHub: https://github.com/TanStack/router
- Zod Documentation: https://zod.dev
- Cloudflare Workers Documentation: https://developers.cloudflare.com/workers/
Last updated: 17. August 2026 Changelog