Typsicherheit

TanStack Router ist darauf ausgelegt, im Rahmen des TypeScript-Compilers und der Laufzeit so typsicher wie möglich zu sein. Das bedeutet, dass er nicht nur in TypeScript geschrieben ist, sondern auch die ihm bereitgestellten Typen vollständig ableitet und sie beharrlich durch die gesamte Routing-Erfahrung leitet.

Letztendlich bedeutet dies, dass Sie als Entwickler weniger Typen schreiben und mehr Vertrauen in Ihren Code haben, während er sich weiterentwickelt.

Routendefinitionen

Dateibasierte Routen

Routen sind hierarchisch, und das gilt auch für ihre Definitionen. Wenn Sie dateibasierte Routen verwenden, ist ein Großteil der Typsicherheit bereits für Sie erledigt.

Code-basierte Routen

Wenn Sie die Route-Klasse direkt verwenden, müssen Sie wissen, wie Sie sicherstellen können, dass Ihre Routen korrekt typisiert sind, indem Sie die Option getParentRoute der Route verwenden. Dies liegt daran, dass untergeordnete Routen über alle ihre übergeordneten Routentypen informiert werden müssen. Ohne dies würden die wertvollen Suchparameter, die Sie aus Ihren Layout- und pfadlosen Layout-Routen, 3 Ebenen darüber, extrahiert haben, im JavaScript-Nirwana verloren gehen.

Vergessen Sie also nicht, die übergeordnete Route an Ihre untergeordneten Routen zu übergeben!

tsx
const parentRoute = createRoute({
  getParentRoute: () => parentRoute,
})
const parentRoute = createRoute({
  getParentRoute: () => parentRoute,
})

Exportierte Hooks, Komponenten und Dienstprogramme

Damit die Typen Ihres Routers mit Top-Level-Exports wie Link, useNavigate, useParams usw. funktionieren, müssen sie die TypeScript-Modulgrenze überschreiten und direkt in die Bibliothek registriert werden. Dazu verwenden wir Deklarationsverschmelzung auf der exportierten Register-Schnittstelle.

ts
const router = createRouter({
  // ...
})

declare module '@tanstack/solid-router' {
  interface Register {
    router: typeof router
  }
}
const router = createRouter({
  // ...
})

declare module '@tanstack/solid-router' {
  interface Register {
    router: typeof router
  }
}

Indem Sie Ihren Router beim Modul registrieren, können Sie nun die exportierten Hooks, Komponenten und Dienstprogramme mit den exakten Typen Ihres Routers verwenden.

Behebung des Komponentenkontextproblems

Der Komponentenkontext ist ein hervorragendes Werkzeug in React und anderen Frameworks, um Komponenten Abhängigkeiten bereitzustellen. Wenn sich jedoch die Typen dieses Kontexts beim Durchlaufen Ihrer Komponentenhierarchie ändern, ist es für TypeScript unmöglich zu wissen, wie diese Änderungen abgeleitet werden sollen. Um dies zu umgehen, erfordern kontextbasierte Hooks und Komponenten, dass Sie ihnen einen Hinweis geben, wie und wo sie verwendet werden.

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

function PostsComponent() {
  // Each route has type-safe versions of most of the built-in hooks from TanStack Router
  const params = Route.useParams()
  const search = Route.useSearch()

  // Some hooks require context from the *entire* router, not just the current route. To achieve type-safety here,
  // we must pass the `from` param to tell the hook our relative position in the route hierarchy.
  const navigate = useNavigate({ from: Route.fullPath })
  // ... etc
}
export const Route = createFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  // Each route has type-safe versions of most of the built-in hooks from TanStack Router
  const params = Route.useParams()
  const search = Route.useSearch()

  // Some hooks require context from the *entire* router, not just the current route. To achieve type-safety here,
  // we must pass the `from` param to tell the hook our relative position in the route hierarchy.
  const navigate = useNavigate({ from: Route.fullPath })
  // ... etc
}

Jeder Hook und jede Komponente, die einen Kontext-Hinweis benötigt, verfügt über einen from-Parameter, an den Sie die ID oder den Pfad der Route übergeben können, innerhalb derer Sie rendern.

🧠 Schneller Tipp: Wenn Ihre Komponente Code-Split ist, können Sie die getRouteApi Funktion verwenden, um die Übergabe von Route.fullPath zu vermeiden und Zugriff auf die typisierten Hooks useParams() und useSearch() zu erhalten.

Was, wenn ich die Route nicht kenne? Was, wenn es eine gemeinsame Komponente ist?

Die from-Eigenschaft ist optional. Wenn Sie sie nicht übergeben, erhalten Sie die bestmögliche Vermutung des Routers, welche Typen verfügbar sein werden. Normalerweise erhalten Sie dann eine Union aller Typen aller Routen im Router.

Was, wenn ich den falschen from-Pfad übergebe?

Es ist technisch möglich, einen from zu übergeben, der TypeScript erfüllt, aber zur Laufzeit nicht der tatsächlichen Route entspricht, innerhalb derer Sie rendern. In diesem Fall erkennen jeder Hook und jede Komponente, die from unterstützen, ob Ihre Erwartungen nicht mit der tatsächlichen Route übereinstimmen, die Sie rendern, und lösen einen Laufzeitfehler aus.

Was, wenn ich die Route nicht kenne oder es eine gemeinsame Komponente ist und ich from nicht übergeben kann?

Wenn Sie eine Komponente rendern, die über mehrere Routen hinweg gemeinsam genutzt wird, oder wenn Sie eine Komponente rendern, die sich nicht innerhalb einer Route befindet, können Sie anstelle einer from-Option strict: false übergeben. Dies unterdrückt nicht nur den Laufzeitfehler, sondern liefert Ihnen auch gelockerte, aber genaue Typen für den potenziellen Hook, den Sie aufrufen. Ein gutes Beispiel hierfür ist der Aufruf von useSearch aus einer gemeinsamen Komponente.

tsx
function MyComponent() {
  const search = useSearch({ strict: false })
}
function MyComponent() {
  const search = useSearch({ strict: false })
}

In diesem Fall wird die Variable search als Union aller möglichen Suchparameter aller Routen im Router typisiert.

Router-Kontext

Der Router-Kontext ist äußerst nützlich, da er die ultimative hierarchische Abhängigkeitsinjektion darstellt. Sie können dem Router und jeder einzelnen von ihm gerenderten Route Kontext bereitstellen. Während Sie diesen Kontext aufbauen, wird TanStack Router ihn mit der Hierarchie der Routen zusammenführen, sodass jede Route Zugriff auf den Kontext all ihrer Eltern hat.

Der Factory createRootRouteWithContext erstellt einen neuen Router mit dem instanziierten Typ, was dann wiederum eine Anforderung schafft, dass Sie denselben Typvertrag für Ihren Router erfüllen müssen. Außerdem wird sichergestellt, dass Ihr Kontext im gesamten Routenbaum korrekt typisiert ist.

tsx
const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
  component: App,
})

const routeTree = rootRoute.addChildren([
  // ... all child routes will have access to `whateverYouWant` in their context
])

const router = createRouter({
  routeTree,
  context: {
    // This will be required to be passed now
    whateverYouWant: true,
  },
})
const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
  component: App,
})

const routeTree = rootRoute.addChildren([
  // ... all child routes will have access to `whateverYouWant` in their context
])

const router = createRouter({
  routeTree,
  context: {
    // This will be required to be passed now
    whateverYouWant: true,
  },
})

Leistungsempfehlungen

Wenn Ihre Anwendung skaliert, werden die TypeScript-Prüfzeiten naturgemäß zunehmen. Es gibt ein paar Dinge zu beachten, wenn Ihre Anwendung skaliert, um die TS-Prüfzeiten zu reduzieren.

Leite nur die benötigten Typen ab

Ein großartiges Muster bei Client-seitigen Daten-Caches (TanStack Query usw.) ist das Vorabladen von Daten. Zum Beispiel könnten Sie mit TanStack Query eine Route haben, die queryClient.ensureQueryData in einem loader aufruft.

tsx
export const Route = createFileRoute('/posts/$postId/deep')({
  loader: ({ context: { queryClient }, params: { postId } }) =>
    queryClient.ensureQueryData(postQueryOptions(postId)),
  component: PostDeepComponent,
})

function PostDeepComponent() {
  const params = Route.useParams()
  const data = useSuspenseQuery(postQueryOptions(params.postId))

  return <></>
}
export const Route = createFileRoute('/posts/$postId/deep')({
  loader: ({ context: { queryClient }, params: { postId } }) =>
    queryClient.ensureQueryData(postQueryOptions(postId)),
  component: PostDeepComponent,
})

function PostDeepComponent() {
  const params = Route.useParams()
  const data = useSuspenseQuery(postQueryOptions(params.postId))

  return <></>
}

Dies mag in Ordnung aussehen und für kleine Routenbäume bemerken Sie möglicherweise keine TS-Leistungsprobleme. In diesem Fall muss TS den Rückgabetyp des Loaders ableiten, obwohl er in Ihrer Route nie verwendet wird. Wenn die Loader-Daten ein komplexer Typ sind und viele Routen auf diese Weise vorab geladen werden, kann dies die Editorleistung beeinträchtigen. In diesem Fall ist die Änderung sehr einfach und lässt TypeScript das Promise ableiten.

tsx
export const Route = createFileRoute('/posts/$postId/deep')({
  loader: async ({ context: { queryClient }, params: { postId } }) => {
    await queryClient.ensureQueryData(postQueryOptions(postId))
  },
  component: PostDeepComponent,
})

function PostDeepComponent() {
  const params = Route.useParams()
  const data = useSuspenseQuery(postQueryOptions(params.postId))

  return <></>
}
export const Route = createFileRoute('/posts/$postId/deep')({
  loader: async ({ context: { queryClient }, params: { postId } }) => {
    await queryClient.ensureQueryData(postQueryOptions(postId))
  },
  component: PostDeepComponent,
})

function PostDeepComponent() {
  const params = Route.useParams()
  const data = useSuspenseQuery(postQueryOptions(params.postId))

  return <></>
}

Auf diese Weise werden die Loader-Daten nie abgeleitet und die Ableitung wird vom Routenbaum auf den ersten Aufruf von useSuspenseQuery verlagert.

Schränke die relevanten Routen so weit wie möglich ein

Betrachten Sie die folgende Verwendung von Link

tsx
<Link to=".." search={{ page: 0 }} />
<Link to="." search={{ page: 0 }} />
<Link to=".." search={{ page: 0 }} />
<Link to="." search={{ page: 0 }} />

Diese Beispiele sind schlecht für die TS-Leistung. Das liegt daran, dass search zu einer Union aller search-Parameter für alle Routen aufgelöst wird und TS prüfen muss, was auch immer Sie an die search-Prop übergeben, gegen diese potenziell große Union. Wenn Ihre Anwendung wächst, steigt diese Prüfzeit linear mit der Anzahl der Routen und Suchparameter. Wir haben unser Bestes getan, um diesen Fall zu optimieren (TypeScript wird diese Arbeit normalerweise einmal erledigen und zwischenspeichern), aber die anfängliche Prüfung gegen diese große Union ist teuer. Dies gilt auch für params und andere APIs wie useSearch, useParams, useNavigate usw.

Stattdessen sollten Sie versuchen, mit from oder to auf relevante Routen einzuschränken.

tsx
<Link from={Route.fullPath} to=".." search={{page: 0}} />
<Link from="/posts" to=".." search={{page: 0}} />
<Link from={Route.fullPath} to=".." search={{page: 0}} />
<Link from="/posts" to=".." search={{page: 0}} />

Denken Sie daran, dass Sie jederzeit eine Union an to oder from übergeben können, um die Routen einzugrenzen, an denen Sie interessiert sind.

tsx
const from: '/posts/$postId/deep' | '/posts/' = '/posts/'
<Link from={from} to='..' />
const from: '/posts/$postId/deep' | '/posts/' = '/posts/'
<Link from={from} to='..' />

Sie können auch Zweige an from übergeben, um search oder params nur von Nachkommen dieses Zweigs ableiten zu lassen.

tsx
const from = '/posts'
<Link from={from} to='..' />
const from = '/posts'
<Link from={from} to='..' />

/posts könnte ein Zweig mit vielen Nachkommen sein, die denselben search oder params teilen.

Erwägen Sie die Verwendung der Objekt-Syntax von addChildren

Es ist typisch, dass Routen params search, loader oder context haben, die sogar auf externe Abhängigkeiten verweisen können und ebenfalls auf TS-Ableitung setzen. Für solche Anwendungen kann die Verwendung von Objekten zur Erstellung des Routenbaums performanter sein als Tupel.

createChildren kann auch ein Objekt akzeptieren. Für große Routenbäume mit komplexen Routen und externen Bibliotheken können Objekte für die Typüberprüfung durch TS wesentlich schneller sein als große Tupel. Die Leistungsgewinne hängen von Ihrem Projekt, Ihren externen Abhängigkeiten und der Art und Weise ab, wie die Typen dieser Bibliotheken geschrieben sind.

tsx
const routeTree = rootRoute.addChildren({
  postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
  indexRoute,
})
const routeTree = rootRoute.addChildren({
  postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
  indexRoute,
})

Beachten Sie, dass diese Syntax zwar ausführlicher ist, aber eine bessere TS-Leistung bietet. Bei der dateibasierten Routenplanung wird der Routenbaum für Sie generiert, sodass ein ausführlicher Routenbaum kein Problem darstellt.

Vermeiden Sie interne Typen ohne Einschränkung

Es ist üblich, dass Sie Typen wiederverwenden möchten, die verfügbar sind. Zum Beispiel könnten Sie versucht sein, LinkProps wie folgt zu verwenden

tsx
const props: LinkProps = {
  to: '/posts/',
}

return (
  <Link {...props}>
)
const props: LinkProps = {
  to: '/posts/',
}

return (
  <Link {...props}>
)

Dies ist SEHR schlecht für die TS-Leistung. Das Problem hier ist, dass LinkProps keine Typargumente hat und daher ein extrem großer Typ ist. Er enthält search, das eine Union aller search-Parameter ist, und er enthält params, das eine Union aller params ist. Beim Zusammenführen dieses Objekts mit Link wird eine strukturelle Gegenüberstellung dieses riesigen Typs durchgeführt.

Stattdessen können Sie as const satisfies verwenden, um einen präzisen Typ abzuleiten und nicht direkt LinkProps, um die riesige Prüfung zu vermeiden.

tsx
const props = {
  to: '/posts/',
} as const satisfies LinkProps

return (
  <Link {...props}>
)
const props = {
  to: '/posts/',
} as const satisfies LinkProps

return (
  <Link {...props}>
)

Da props nicht vom Typ LinkProps ist, ist diese Prüfung günstiger, da der Typ viel präziser ist. Sie können die Typüberprüfung weiter verbessern, indem Sie LinkProps einschränken.

tsx
const props = {
  to: '/posts/',
} as const satisfies LinkProps<RegisteredRouter, string '/posts/'>

return (
  <Link {...props}>
)
const props = {
  to: '/posts/',
} as const satisfies LinkProps<RegisteredRouter, string '/posts/'>

return (
  <Link {...props}>
)

Dies ist noch schneller, da wir gegen den eingeschränkten LinkProps-Typ prüfen.

Sie können dies auch verwenden, um den Typ von LinkProps auf einen bestimmten Typ einzuschränken, der als Prop oder Parameter einer Funktion verwendet werden kann.

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

export type MyLinkProps = (typeof myLinkProps)[number]

const MyComponent = (props: { linkProps: MyLinkProps }) => {
  return <Link {...props.linkProps} />
}
export const myLinkProps = [
  {
    to: '/posts',
  },
  {
    to: '/posts/$postId',
    params: { postId: 'postId' },
  },
] as const satisfies ReadonlyArray<LinkProps>

export type MyLinkProps = (typeof myLinkProps)[number]

const MyComponent = (props: { linkProps: MyLinkProps }) => {
  return <Link {...props.linkProps} />
}

Dies ist schneller als die direkte Verwendung von LinkProps in einer Komponente, da MyLinkProps ein viel präziserer Typ ist.

Eine weitere Lösung besteht darin, nicht LinkProps zu verwenden und stattdessen eine umgekehrte Steuerung zu implementieren, um eine Link-Komponente bereitzustellen, die auf eine bestimmte Route eingeschränkt ist. Render Props sind eine gute Methode, um die Steuerung an den Benutzer einer Komponente umzukehren.

tsx
export interface MyComponentProps {
  readonly renderLink: () => React.ReactNode
}

const MyComponent = (props: MyComponentProps) => {
  return <div>{props.renderLink()}</div>
}

const Page = () => {
  return <MyComponent renderLink={() => <Link to="/absolute" />} />
}
export interface MyComponentProps {
  readonly renderLink: () => React.ReactNode
}

const MyComponent = (props: MyComponentProps) => {
  return <div>{props.renderLink()}</div>
}

const Page = () => {
  return <MyComponent renderLink={() => <Link to="/absolute" />} />
}

Dieses spezielle Beispiel ist sehr schnell, da wir die Steuerung, wohin wir navigieren, an den Benutzer der Komponente umgekehrt haben. Der Link ist auf die exakte Route eingeschränkt, zu der wir navigieren möchten.

Unsere Partner
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Bytes abonnieren

Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.

Bytes

Kein Spam. Jederzeit kündbar.

Bytes abonnieren

Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.

Bytes

Kein Spam. Jederzeit kündbar.