Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

Type-Safe Routing в TanStack Router: как настроить строгую типизацию путей и параметров

Тимофей Ищенко

Коротко: Подробное руководство по настройке строгой типизации путей, динамических и 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

Библиотека поддерживает два формата описания дерева:

  1. File-based routing (рекомендуемый): файловая структура транслируется в сгенерированный файл routeTree.gen.ts с помощью Vite-плагина или CLI-генератора. Типы создаются автоматически при сохранении файлов.
  2. 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.


Настройка проекта: чек-лист типобезопасности

Чтобы окружение работало без сбоев и с максимальной скоростью вывода типов, соблюдайте следующую конфигурацию:

  1. Настройки tsconfig.json: Убедитесь, что в конфигурации TypeScript включены флаги строгого режима:

    {
      "compilerOptions": {
        "strict": true,
        "moduleResolution": "Bundler",
        "jsx": "react-jsx",
        "skipLibCheck": true
      }
    }
    
  2. Синхронизация генератора маршрутов: При 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()],
    })
    
  3. Создание переиспользуемых оберток над навигацией: Если в дизайн-системе используется собственный компонент кнопки или ссылки, создавайте его с помощью утилиты 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 в момент написания кода, а не пользователями в продакшене. Строгая типизация путей упрощает рефакторинг крупных проектов и исключает целый класс тривиальных, но опасных дефектов.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также