Коротко: Подробное руководство по настройке строгой типизации путей, динамических и search-параметров в TanStack Router. Примеры валидации со схемами и Declaration Merging.
В классических React-приложениях маршрутизация долгое время оставалась «слепой зоной» для компилятора TypeScript. Переход по ссылкам вида <Link to="/users/123/profile" /> или чтение query-параметров через useSearchParams() традиционно опираются на сырые строки. Опечатка в сегменте URL, пропущенный динамический ID или изменение формата query-параметра легко проходят этап сборки и превращаются в 404-ошибки или падения рантайма у конечного пользователя.
TanStack Router решает эту проблему фундаментально за счет концепции lossless type-inference. Это не просто декоративная обертка над строками, а маршрутизатор, где вся структура дерева, динамические сегменты, схемы параметров и контекст строго типизированы на уровне компилятора.
Архитектура Lossless Type-Inference в TanStack Router
Большинство роутеров теряют контекст типов при разбиении приложения на независимые модули. В TanStack Router информация о типах пронизывает всю кодовую базу — от объявления корневого узла до глубоко вложенных хуков.
Роль интерфейса Register и Declaration Merging
Чтобы компоненты вроде <Link> и хуки useNavigate, useParams, useSearch имели доступ к типам всего дерева маршрутов без ручного прокидывания дженериков в каждом файле, используется механизм Declaration Merging (слияние деклараций TypeScript):
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({ routeTree })
// Регистрация типа роутера для глобального автокомплита
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
После регистрации интерфейса Register глобальный модуль @tanstack/react-router знает точную конфигурацию вашего дерева: допустимые строковые литералы путей, типы динамических параметров для каждого сегмента и форму search-параметров.
File-based routing vs Code-based routing
Библиотека поддерживает два формата описания дерева:
- File-based routing (рекомендуемый): файловая структура транслируется в сгенерированный файл
routeTree.gen.tsс помощью Vite-плагина или CLI-генератора. Типы создаются автоматически при сохранении файлов. - Code-based routing: маршруты объявляются вручную через функции
createRoute. Для сохранения непрерывной цепочки типов каждый дочерний маршрут обязан явно ссылаться на родительский через опциюgetParentRoute:
const rootRoute = createRootRoute()
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
})
const postDetailRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
})
Строгая типизация путей и Path Params
Динамические параметры путей часто становятся источником скрытых дефектов при рефакторинге. В TanStack Router динамический сегмент обозначается символом $ в названии папки/файла или свойства path.
Именование и автоматический вывод параметров
Если маршрут определен как routes/posts/$postId.tsx (или path: '$postId'), роутер автоматически формирует тип { postId: string }.
Попытка перейти по такому маршруту без передачи параметра вызовет ошибку компиляции на этапе написания кода:
import { Link } from '@tanstack/react-router'
export function PostPreview({ id, title }: { id: string; title: string }) {
return (
// TypeScript требует строго указать params с ключом postId
<Link
to="/posts/$postId"
params={{ postId: id }}
className="text-blue-600 hover:underline"
>
{title}
</Link>
)
}
Если опечататься в ключе (например, написать params={{ id }}), TypeScript не соберет проект и подсветит отсутствующее обязательное свойство postId.
Валидация и трансформация через parseParams
По умолчанию URL-сегменты всегда являются строками. Если сущность в базе данных или API идентифицируется числом, TanStack Router позволяет валидировать и преобразовывать типы прямо в определении маршрута:
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const paramsSchema = z.object({
postId: z.coerce.number().int().positive(),
})
export const Route = createFileRoute('/posts/$postId')({
params: {
parse: (rawParams) => paramsSchema.parse(rawParams),
stringify: ({ postId }) => ({ postId: `${postId}` }),
},
component: PostComponent,
})
function PostComponent() {
// postId автоматически имеет тип number, а не string
const { postId } = Route.useParams()
return <div>ID статьи: {postId}</div>
}
Type-Safe Search Params: query-параметры как состояние приложения
Работа с URL query string в стандартном вебе сопряжена с рядом проблем: массивы сериализуются неоднозначно, числа превращаются в строки, а невалидные значения требуют ручной обработки на каждом экране.
В TanStack Router параметры поиска рассматриваются как полноценный типобезопасный стейт.
Валидация через validateSearch
Свойство validateSearch маршрута принимает функцию-валидатор. В нее передается сырой объект параметров, а на выходе возвращается строго типизированный результат. Для описания схем отлично подходят библиотеки вроде Zod, Valibot или ArkType:
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const catalogSearchSchema = z.object({
page: z.number().catch(1),
query: z.string().optional(),
sortBy: z.enum(['price_asc', 'price_desc', 'newest']).default('newest'),
inStockOnly: z.boolean().catch(false),
})
export const Route = createFileRoute('/catalog')({
validateSearch: (search) => catalogSearchSchema.parse(search),
component: CatalogPage,
})
При использовании .catch() или .default() из Zod роутер автоматически нормализует битые query-параметры в URL, предотвращая падение интерфейса.
Чтение и обновление Search Params
Для чтения данных внутри компонента используется хук Route.useSearch(). Для обновления параметров через useNavigate или <Link> передается функция модификации:
function CatalogPage() {
const { page, sortBy, query } = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
const handlePageChange = (newPage: number) => {
navigate({
search: (prev) => ({
...prev,
page: newPage,
}),
})
}
return (
<div>
<input
type="text"
value={query ?? ''}
onChange={(e) =>
navigate({
search: (prev) => ({
...prev,
query: e.target.value || undefined, // undefined удаляет ключ из URL
}),
})
}
/>
<p>Текущая страница: {page}, сортировка: {sortBy}</p>
</div>
)
}
TypeScript гарантирует, что в search нельзя передать непредусмотренный ключ или значение некорректного типа (например, передать строку в page).
Маршрутизация на основе контекста (Route Context)
Часто маршрутам требуются внешние сервисы: клиент запросов к данным (React Query / TanStack Query), инстанс аутентификации или общая конфигурация приложения. TanStack Router позволяет передавать контекст сверху вниз с сохранением полной типизации.
import { createRootRouteWithContext, createRoute, redirect } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
interface MyRouterContext {
queryClient: QueryClient
auth: {
isAuthenticated: boolean
userId: string | null
}
}
// 1. Создаем корневой маршрут с описанием типов контекста
export const rootRoute = createRootRouteWithContext<MyRouterContext>()()
// 2. В дочернем маршруте контекст доступен в loader и beforeLoad
export const adminRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'admin',
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
})
При инициализации экземпляра router компилятор потребует обязательно передать объект context, соответствующий заявленному интерфейсу MyRouterContext.
Настройка проекта: чек-лист типобезопасности
Чтобы окружение работало без сбоев и с максимальной скоростью вывода типов, соблюдайте следующую конфигурацию:
Настройки
tsconfig.json: Убедитесь, что в конфигурации TypeScript включены флаги строгого режима:{ "compilerOptions": { "strict": true, "moduleResolution": "Bundler", "jsx": "react-jsx", "skipLibCheck": true } }Синхронизация генератора маршрутов: При File-based подходе используйте
@tanstack/router-plugin/viteвvite.config.ts. Это гарантирует обновлениеrouteTree.gen.tsна лету в процессе разработки без необходимости ручного перезапуска сборщика:import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { TanStackRouterVite } from '@tanstack/router-plugin/vite' export default defineConfig({ plugins: [TanStackRouterVite(), react()], })Создание переиспользуемых оберток над навигацией: Если в дизайн-системе используется собственный компонент кнопки или ссылки, создавайте его с помощью утилиты
createLink:import { createLink } from '@tanstack/react-router' import { CustomButton } from '@/components/ui/Button' export const RouterButton = createLink(CustomButton)Компонент
RouterButtonавтоматически унаследует все свойства типизации путей (to,params,search).
Часто задаваемые вопросы (FAQ)
1. Зачем регистрировать роутер через declare module '@tanstack/react-router'?
Это активирует механизм Declaration Merging в TypeScript. Глобальные компоненты (Link) и хуки (useNavigate, useRouter) связываются со сгенерированным деревом конкретного проекта без необходимости явно прописывать дженерики при каждом вызове.
2. Что происходит при ручном вводе некорректных Search Params в адресную строку?
Если в схеме валидации (например, через Zod) заданы дефолтные значения или методы восстановления вроде .catch(), роутер перехватит ошибку, подставит валидные fallback-значения и автоматически скорректирует адресную строку без прерывания рендеринга.
3. Обязательно ли использовать File-based Routing для полной типизации?
Нет. Code-based routing обеспечивает точно такой же уровень строгой типизации. Однако при росте кодовой базы File-based подход значительно удобнее, так как избавляет от ручного связывания маршрутов через getParentRoute.
4. Как типизировать относительные переходы (relative navigation)?
В компонентах <Link> и хуке useNavigate можно передать свойство from. В этом случае роутер будет знать точную текущую позицию в дереве и предложит строгий автокомплит для относительных путей (например, to: '../edit').
5. Сильно ли усложнение типов влияет на скорость работы TypeScript Language Server?
Архитектура типов TanStack Router оптимизирована под минимизацию рекурсивных вычислений. При соблюдении современных рекомендаций компилятора (moduleResolution: "Bundler", актуальная версия TS 5.x) задержки автокомплита остаются незаметными даже на проектах с сотнями маршрутов.
6. Можно ли использовать другие библиотеки валидации вместо Zod?
Да. Опция validateSearch принимает любую стандартную функцию (rawSearch: Record<string, unknown>) => TOutput. Вы можете использовать Valibot, ArkType, Superstruct или собственную функцию валидации.
Заключение
Интеграция TanStack Router переводит работу с маршрутизацией на качественно новый инженерный уровень. Ошибки в адресах страниц, рассинхронизация параметров поиска и потеря контекста теперь отлавливаются компилятором TypeScript в момент написания кода, а не пользователями в продакшене. Строгая типизация путей упрощает рефакторинг крупных проектов и исключает целый класс тривиальных, но опасных дефектов.




.svg.webp)




