Использование с TypeScript

Universal Router в связке с TypeScript позволяет строго типизировать маршруты, их параметры и возвращаемые значения. Основная идея — определить интерфейсы для параметров каждого маршрута и использовать их при описании маршрутов и вызове router.resolve.

import UniversalRouter, { Route } from 'universal-router';

interface RouteParams {
  id?: string;
  category?: string;
}

const routes: Route<RouteParams>[] = [
  {
    path: '/',
    action: () => 'Главная страница',
  },
  {
    path: '/product/:id',
    action: ({ params }) => `Продукт с ID: ${params.id}`,
  },
  {
    path: '/category/:category',
    action: ({ params }) => `Категория: ${params.category}`,
  },
];

const router = new UniversalRouter<RouteParams>(routes);

(async () => {
  const result = await router.resolve({ pathname: '/product/42' });
  console.log(result); // Продукт с ID: 42
})();

Ключевое здесь — параметризация Route<RouteParams>, которая гарантирует наличие ожидаемых параметров и их типизацию в каждом маршруте. Если попытаться обратиться к несуществующему параметру, TypeScript выдаст ошибку.


Использование Generics для возвращаемых значений

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

interface User {
  id: string;
  name: string;
}

interface UserRouteParams {
  id: string;
}

const userRoutes: Route<UserRouteParams, User>[] = [
  {
    path: '/user/:id',
    async action({ params }) {
      const user: User = await fetchUser(params.id);
      return user;
    },
  },
];

const userRouter = new UniversalRouter<UserRouteParams, User>(userRoutes);

async function fetchUser(id: string): Promise<User> {
  return { id, name: 'Иван' };
}

(async () => {
  const user = await userRouter.resolve({ pathname: '/user/123' });
  console.log(user.name); // Иван
})();

Использование Generics делает работу с данными безопасной и предотвращает типовые ошибки при обращении к свойствам объекта, возвращаемого маршрутом.


Типизация контекста маршрута

Universal Router позволяет передавать контекст, который доступен в каждом маршруте. TypeScript позволяет типизировать этот контекст для строгой проверки.

interface AppContext {
  isAuthenticated: boolean;
  userRole: 'admin' | 'user';
}

interface ContextRouteParams {
  section?: string;
}

const contextRoutes: Route<ContextRouteParams, string, AppContext>[] = [
  {
    path: '/admin',
    action: ({ context }) => {
      if (!context.isAuthenticated || context.userRole !== 'admin') {
        return 'Доступ запрещён';
      }
      return 'Панель администратора';
    },
  },
];

const contextRouter = new UniversalRouter<ContextRouteParams, string, AppContext>(
  contextRoutes
);

(async () => {
  const result = await contextRouter.resolve({ pathname: '/admin', context: { isAuthenticated: true, userRole: 'admin' } });
  console.log(result); // Панель администратора
})();

Контекст позволяет расширять функциональность маршрутов и строго контролировать доступ, сохраняя при этом статическую проверку типов.


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

В Universal Router маршруты могут быть вложенными. TypeScript позволяет сохранять строгую типизацию при использовании вложенных маршрутов.

interface NestedParams {
  section: string;
  page?: string;
}

const nestedRoutes: Route<NestedParams>[] = [
  {
    path: '/docs',
    children: [
      {
        path: ':section',
        action: ({ params }) => `Документация: ${params.section}`,
        children: [
          {
            path: ':page',
            action: ({ params }) => `Страница: ${params.page} в разделе ${params.section}`,
          },
        ],
      },
    ],
  },
];

const nestedRouter = new UniversalRouter<NestedParams>(nestedRoutes);

(async () => {
  const result = await nestedRouter.resolve({ pathname: '/docs/javascript/intro' });
  console.log(result); // Страница: intro в разделе javascript
})();

Вложенные маршруты с типизацией гарантируют корректность параметров на каждом уровне маршрута, предотвращая ошибки доступа к неопределённым параметрам.


Асинхронные маршруты и типизация Promise

Universal Router позволяет маршрутам быть асинхронными. TypeScript позволяет точно указать, что возвращаемое значение является Promise<T> и какие данные оно содержит.

interface AsyncParams {
  id: string;
}

interface AsyncData {
  title: string;
  content: string;
}

const asyncRoutes: Route<AsyncParams, Promise<AsyncData>>[] = [
  {
    path: '/article/:id',
    async action({ params }) {
      const data: AsyncData = await fetchArticle(params.id);
      return data;
    },
  },
];

async function fetchArticle(id: string): Promise<AsyncData> {
  return { title: `Статья ${id}`, content: 'Содержимое статьи' };
}

const asyncRouter = new UniversalRouter<AsyncParams, Promise<AsyncData>>(asyncRoutes);

(async () => {
  const article = await asyncRouter.resolve({ pathname: '/article/10' });
  console.log(article.title); // Статья 10
})();

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


Типизация вспомогательных функций маршрутов

При работе с Universal Router часто создаются вспомогательные функции для генерации путей и параметров. Использование TypeScript позволяет типизировать эти функции для согласованности с маршрутами.

interface ProfileParams {
  userId: string;
}

const profileRoutes: Route<ProfileParams>[] = [
  { path: '/profile/:userId', action: () => 'Профиль пользователя' },
];

function buildProfilePath(params: ProfileParams): string {
  return `/profile/${params.userId}`;
}

const profileRouter = new UniversalRouter<ProfileParams>(profileRoutes);

const path = buildProfilePath({ userId: '42' });
console.log(path); // /profile/42

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


Интеграция с React и TypeScript

Universal Router хорошо интегрируется с React, а TypeScript позволяет строго типизировать возвращаемые компоненты и контекст.

import React from 'react';
import UniversalRouter, { Route } from 'universal-router';

interface ReactParams {
  page: string;
}

const routes: Route<ReactParams, React.ReactNode>[] = [
  {
    path: '/home',
    action: () => <h1>Главная страница</h1>,
  },
  {
    path: '/about',
    action: () => <h1>О нас</h1>,
  },
];

const router = new UniversalRouter<ReactParams, React.ReactNode>(routes);

(async () => {
  const component = await router.resolve({ pathname: '/about' });
  // component можно напрямую рендерить в React
})();

Типизация компонентов предотвращает ошибки при интеграции маршрутизатора с системой рендеринга и позволяет использовать преимущества автодополнения в редакторах кода.