Строгая типизация навигации

React Router предоставляет мощный механизм для организации маршрутизации в приложениях на React. Однако при построении крупных приложений становится критически важной строгая типизация навигации для предотвращения ошибок при передаче параметров, формировании URL и работе с историей навигации. Использование TypeScript совместно с React Router позволяет обеспечить безопасное взаимодействие между компонентами и маршрутами на этапе компиляции.


Типизация маршрутов

В React Router маршруты определяются с помощью компонента <Route>. Основные параметры маршрута — path, element и children. Для строгой типизации можно определить объект с маршрутами и их параметрами:

interface RouteParams {
  home: {};
  user: { userId: string };
  settings: { tab?: string };
}

type RouteKey = keyof RouteParams;

const routes: Record<RouteKey, string> = {
  home: "/",
  user: "/user/:userId",
  settings: "/settings/:tab?"
};

Ключевые моменты:

  • Record<RouteKey, string> связывает ключ маршрута с шаблоном URL.
  • Параметры в RouteParams описывают обязательные и опциональные параметры.
  • Использование шаблонов URL (:param) обеспечивает согласованность с определением маршрута.

Навигация с типами

Для безопасной навигации применяется useNavigate с вспомогательной функцией, которая учитывает параметры:

import { useNavigate } from "react-router-dom";

function useTypedNavigate() {
  const navigate = useNavigate();

  return function <T extends RouteKey>(
    route: T,
    params: RouteParams[T]
  ) {
    let path = routes[route];
    for (const key in params) {
      if (params[key]) {
        path = path.replace(`:${key}`, params[key] as string);
      }
    }
    navigate(path);
  };
}

Особенности подхода:

  • Параметры маршрута строго соответствуют интерфейсу RouteParams.
  • Любая попытка передать неверный параметр приведет к ошибке компиляции.
  • Динамическое формирование URL исключает ручное построение строк.

Типизация параметров URL

React Router предоставляет хук useParams для получения параметров маршрута. Для строгой типизации можно использовать дженерики:

import { useParams } from "react-router-dom";

function UserPage() {
  const params = useParams<{ userId: string }>();
  return <div>User ID: {params.userId}</div>;
}

Преимущества:

  • Доступ к параметрам с автодополнением в редакторе.
  • Компилятор проверяет наличие обязательных параметров.
  • Исключается использование any и последующие ошибки времени выполнения.

Типизация query-параметров

Для работы с query-параметрами используется объект URLSearchParams. Строгая типизация возможна через отдельные интерфейсы:

interface UserQuery {
  showDetails: boolean;
}

function useTypedQuery<T>() {
  const params = new URLSearchParams(window.location.search);
  return new Proxy(params, {
    get(target, prop: string) {
      const value = target.get(prop);
      return typeof value === "string" ? value : undefined;
    }
  }) as unknown as T;
}

function UserPage() {
  const query = useTypedQuery<UserQuery>();
  if (query.showDetails) {
    return <div>Подробности пользователя</div>;
  }
  return <div>Общая информация</div>;
}

Заметки по реализации:

  • Преобразование строковых значений query-параметров в нужный тип требует явного приведения.
  • Использование прокси обеспечивает автодополнение и строгие ключи.
  • Типизация query-параметров помогает избегать ошибок при работе с фильтрацией и сортировкой.

Типизация состояния при навигации

React Router позволяет передавать состояние через navigate:

interface NavigationState {
  fromDashboard?: boolean;
}

const navigate = useNavigate();
navigate("/settings", { state: { fromDashboard: true } });

Для строгой типизации рекомендуется использовать дженерики:

function useTypedNavigateState<T>() {
  const navigate = useNavigate();
  return (path: string, state: T) => navigate(path, { state });
}

const typedNavigate = useTypedNavigateState<NavigationState>();
typedNavigate("/settings", { fromDashboard: true });

Преимущества:

  • Строгая проверка структуры состояния при навигации.
  • Компилятор контролирует соответствие переданных данных интерфейсу.
  • Уменьшается риск ошибок при чтении состояния через useLocation.

Интеграция с компонентами Route

Использование типизированных маршрутов облегчает построение компонента <Routes>:

import { Routes, Route } from "react-router-dom";

function AppRoutes() {
  return (
    <Routes>
      <Route path={routes.home} element={<HomePage />} />
      <Route path={routes.user} element={<UserPage />} />
      <Route path={routes.settings} element={<SettingsPage />} />
    </Routes>
  );
}

Преимущества:

  • Все маршруты централизованно хранятся в объекте routes.
  • Изменение шаблона URL автоматически обновляется во всех местах использования.
  • Строгая типизация параметров предотвращает несоответствие между URL и компонентами.

Резюме подхода

Строгая типизация навигации в React Router позволяет:

  • Исключить ошибки времени выполнения при передаче параметров и состоянии.
  • Получить автодополнение и проверку типов в редакторе.
  • Централизованно управлять маршрутами, параметрами и query-параметрами.
  • Обеспечить совместимость с TypeScript без потери гибкости маршрутизации.

Такой подход особенно важен для крупных приложений с множеством динамических маршрутов, где контроль типов становится ключевым инструментом стабильной разработки.