Генерация типов из маршрутов

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

Структура маршрутов

В React Router маршруты обычно описываются с помощью компонента <Routes> и вложенных <Route>. Пример базовой структуры:

import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Profile from './pages/Profile';

const AppRoutes = () => (
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/profile/:userId" element={<Profile />} />
  </Routes>
);

Здесь path задаёт URL, а element — компонент, который рендерится при совпадении пути. В случае динамических сегментов, таких как :userId, важно корректно типизировать параметры маршрута.

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

TypeScript позволяет извлекать параметры маршрутов и строго типизировать их, используя хук useParams. Пример:

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

interface ProfileParams {
  userId: string;
}

const Profile = () => {
  const params = useParams<ProfileParams>();
  return <div>Профиль пользователя {params.userId}</div>;
};

Ключевым моментом здесь является интерфейс ProfileParams, который описывает все динамические сегменты URL. Это исключает возможность опечаток при обращении к параметрам маршрута.

Генерация типов из конфигурации маршрутов

В больших приложениях маршруты часто описываются в виде конфигурационного массива. Это позволяет централизовать маршруты и использовать их повторно. Пример:

const routesConfig = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/profile/:userId', component: Profile },
] as const;

Использование as const превращает массив в литеральный тип, что позволяет TypeScript точно определить возможные пути. Далее можно автоматически генерировать типы для path и параметров маршрута.

type RoutePath = typeof routesConfig[number]['path'];

Теперь RoutePath будет строго соответствовать всем указанным маршрутам: '/' | '/about' | '/profile/:userId'.

Генерация типов параметров динамических сегментов

Для динамических маршрутов важно автоматически извлекать параметры. Это можно сделать с помощью условных типов и шаблонных литералов TypeScript:

type ExtractRouteParams<T extends string> = 
  T extends `${infer _Start}/:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<`/${Rest}`>
    : T extends `${infer _Start}/:${infer Param}`
      ? Param
      : never;

type ProfileRouteParams = ExtractRouteParams<'/profile/:userId'>; // "userId"

Эта конструкция рекурсивно обрабатывает строку маршрута и извлекает все динамические сегменты, превращая их в объединение строковых литералов. Для маршрута с несколькими параметрами тип будет автоматически включать все ключи.

Интеграция с функцией навигации

Генерация типов позволяет безопасно строить функции навигации. Вместо передачи строки вручную используется строго типизированная функция:

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

const useTypedNavigate = () => {
  const navigate = useNavigate();
  return <T extends RoutePath>(path: T, params?: Record<ExtractRouteParams<T>, string>) => {
    let resolvedPath = path;
    if (params) {
      for (const key in params) {
        resolvedPath = resolvedPath.replace(`:${key}`, params[key]);
      }
    }
    navigate(resolvedPath);
  };
};

// Пример использования:
const goToProfile = useTypedNavigate();
goToProfile('/profile/:userId', { userId: '123' });

Такой подход обеспечивает компиляторную проверку корректности маршрутов и параметров. Любая ошибка, например опечатка в параметре, будет выявлена на этапе компиляции.

Генерация типов для вложенных маршрутов

Вложенные маршруты часто встречаются в больших приложениях. Например:

const routesConfig = [
  {
    path: '/dashboard',
    component: Dashboard,
    children: [
      { path: 'analytics', component: Analytics },
      { path: 'reports', component: Reports },
    ],
  },
] as const;

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

type NestedRoutePath<T> = T extends { path: infer P; children: infer C }
  ? P | NestedRoutePath<C[number]>
  : never;

type AppRoutePaths = NestedRoutePath<typeof routesConfig>; 
// "/dashboard" | "analytics" | "reports"

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

Преимущества генерации типов из маршрутов

  • Безопасность навигации: невозможно передать несуществующий путь или неправильный параметр.
  • Автодополнение: IDE предлагает варианты маршрутов и параметров.
  • Поддержка изменений: при добавлении нового маршрута автоматически обновляются все типы, связанные с маршрутами.
  • Снижение ошибок: опечатки в строках маршрутов становятся невозможны на этапе компиляции.

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