
type-safety
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.
- Installs
- 207
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/tanstack/router --skill type-safety- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads type-safety automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
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,satisfieson 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 nogetServerSideProps, nouseSearchParams(), nouseLoaderData()fromreact-router-dom.
The ONE Required Type Annotation: Register
Without this, top-level exports like Link, useNavigate, useSearch have no type safety.
// 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
// 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
// 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 />,
})
// 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
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
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:
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:
// 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
// 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:
// SLOWER — TS infers the full query return type into the route tree
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context: { queryClient }, params: { postId } }) =>
queryClient.query({ ...postQueryOptions(postId), staleTime: 'static' }),
component: PostComponent,
})
// FASTER — async loader returns no query data, inference stays out of the route tree
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient }, params: { postId: id } }) => {
await queryClient.query({ ...postQueryOptions(id), staleTime: 'static' })
},
component: PostComponent,
})
as const satisfies for Link Option Objects
Never use LinkProps as a variable type — it's an enormous union:
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
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
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
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:
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.
Render Props for Maximum Performance
Instead of accepting LinkProps, invert control so Link is narrowed at the call site:
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
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:
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
// 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
// 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
// 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
// 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
Wrong-framework code looks plausible (it's React) but breaks the build or produces conflicting / routes at runtime.
// 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'
// 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.
6. CRITICAL: Treating typecheck as proof of runtime schema propagation
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).
FAQ
What does the type-safety skill do?
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.
How do I install type-safety?
Run npx skills add https://github.com/tanstack/router --skill type-safety in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/type-safety/ (personal) or .claude/skills/type-safety/ (project).
Which AI agents can use type-safety?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is type-safety safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains type-safety?
It is published by tanstack in tanstack/router. It has 207 recorded installs.
More from tanstack/router
router-core
tanstack/router
Framework-agnostic core concepts for TanStack Router: route trees, createRouter, createRoute, createRootRoute, createRootRouteWithContext, addChildren, Register type declaration, route matching, route sorting, file naming conventions. Entry point for all router skills.
react-router
tanstack/router
React bindings for TanStack Router: RouterProvider, useRouter, useRouterState, useMatch, useMatches, useLocation, useSearch, useParams, useNavigate, useLoaderData, useLoaderDeps, useRouteContext, useBlocker, useCanGoBack, Link, Navigate, Outlet, CatchBoundary, ErrorComponent. React-specific patterns for hooks, providers, SSR hydration, and createLink with forwardRef.
react-start
tanstack/router
React bindings for TanStack Start: createStart, StartClient, StartServer, React-specific imports, re-exports from @tanstack/react-router, full project setup with React, useServerFn hook.
start-core
tanstack/router
Core overview for TanStack Start: tanstackStart() Vite plugin, getRouter() factory, root route document shell (HeadContent, Scripts, Outlet), client/server entry points, routeTree.gen.ts, tsconfig configuration. Entry point for all Start skills.
start-server-core
tanstack/router
Server-side runtime for TanStack Start: createStartHandler, request/response utilities (getRequest, setResponseHeader, setCookie, getCookie, appendResponseHeader), three-phase request handling, AsyncLocalStorage context.
Popular ai & agents skills
google-agents-cli-publish
google/agents-cli
This skill should be used when the user wants to "publish an agent", "publish my ADK agent", "register an agent with Gemini", "publish to Gemini" (formerly Gemini Enterprise), or needs guidance on the agents-cli publish gemini command. Also use when the user wants to "manage agents in Agent Registry", "list/update/delete registered agents", or "register an MCP server". Covers ADK vs A2A registration modes, programmatic and interactive usage, flag reference, auto-detection from deployment metadata, Agent Registry fleet management, and troubleshooting. Part of the agents-cli skills suite. Do NOT use for deployment (use google-agents-cli-deploy).
google-agents-cli-eval
google/agents-cli
This skill should be used when the user wants to "run an evaluation", "evaluate my agent", "evaluate my ADK agent", "write an eval dataset", "analyze eval failures", "compare eval results", "optimize agent", or needs guidance on the Gemini platform eval methodology and the Quality Flywheel. Covers eval metrics, dataset schema, LLM-as-judge scoring, and common failure causes. Applies to any agents-cli project, whatever framework the agent is written in. Do NOT use for agent API code patterns (ADK: use google-agents-cli-adk-code), deployment (use google-agents-cli-deploy), or project scaffolding (use google-agents-cli-scaffold).
skill-creator
anthropics/skills
Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.
codex-pet
prime-skills/runcomfy-agent-skills
Codex Pet generator on RunComfy. Build a Codex-compatible Codex Pet spritesheet.webp + pet.json from a single reference image, drop it into ${CODEXHOME:-$HOME/.codex}/pets/ / and Codex picks it up as a custom Codex Pet next to the 8 built-ins. This skill produces the exact Codex Pet atlas Codex expects (1536x1872 PNG/WebP, 8 cols x 9 rows, 192x208 cells, 9 animation states — idle, running-right, running-left, waving, jumping, failed, waiting, running, review). Calls OpenAI GPT Image 2 edit ONCE via the local RunComfy CLI as runcomfy run openai/gpt-image-2/edit to produce a canonical Codex Pet pose, then assembles all 9 animation rows programmatically with ImageMagick micro-transforms — no Codex Pro, no $imagegen, no OPENAIAPIKEY required, only RUNCOMFYTOKEN. Triggers on "codex pet", "create codex pet", "make codex pet", "hatch codex pet", "/hatch image", "desktop pet codex", "codex pets", "spritesheet.webp", or any explicit ask to build a custom pet for OpenAI Codex.
full-output-enforcement
leonxlnx/taste-skill
Overrides default LLM truncation behavior. Enforces complete code generation, bans placeholder patterns, and handles token-limit splits cleanly. Apply to any task requiring exhaustive, unabridged output.
executing-plans
obra/superpowers
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available