Навигация без хаоса: архитектура маршрутов в масштабируемом TypeScript-проекте

от автора

Привет! Я Илья, фронтенд-разработчик в финтех-компании Точка. Нам важно, чтобы поддержка пользователей была на высоком уровне, поэтому у нас есть десятки сервисов для организации обучения специалистов поддержки. Я работаю над одним из таких проектов. Он активно развивается: ежемесячно добавляем более 10 новых страниц — сейчас в проекте их больше 120.

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

Примеры в статье написаны на TypeScript, React и React Router, но подход универсален и может применяться в других технологиях:

  • React, Next.js, Vue, Svelte (Frontend).

  • React Native (Mobile).

  • Electron (Desktop).

  • Node.js, NestJS, Express (Backend).

Частая проблема организации путей роутинга

export const MAIN_PAGE_PATH = '/'; export const AUTH_PAGE_PATH = '/login'; export const NOT_FOUND_PAGE_PATH = '/404'; export const SEARCH_PAGE_PATH = '/search'; export const TASKS_PAGE_PATH = '/tasks'; export const ACTUAL_TASKS_PAGE_PATH = '/tasks/actual'; export const FINISHED_TASKS_PAGE_PATH = '/tasks/finished'; export const getTaskPagePath = (id: number, type: string): string => `/tasks/${id}?type=${type}`; // и это хорошо ещё, если конструктор параметров вынесен глобально // ...и здесь ещё сто штук

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

Эти многочисленные константы при вёрстке используются в таком формате:

--------- home-page.tsx: import { useNavigate } from 'react-router-dom'; // используем утилиты для навигации const navigate = useNavigate(); // осуществляем переход на другую страницу, прокидываем параметры navigate(getTaskPagePath(task.id, task.type))  --------- task-page.tsx: import { useParams } from 'react-router'; import { useQueryParams } from 'shared/helpers'; // извлекаем параметры страницы const { id } = useParams<{ id: number }>(); const { type } = useQueryParams<{ type: string }>(); // как-то используем id и type в верстке ...

Поддерживать и развивать такой код непросто. Среди очевидных проблем:

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

  2. Дублирование в нейминге. В проекте могут использоваться константы с суффиксами PAGE, PATH, ROUTE и т.п. Это усложняет чтение и поддержку кода.

  3. Отсутствие связи между параметрами и страницами. Path- и query-параметры определены и в константах путей, и в самих компонентах страниц. В результате дублирования типов увеличивается вероятность несоответствий и ошибок.

Когда маршрутов становится больше, эти сложности накапливаются и негативно влияют на масштабируемость.

Базовое решение проблемы

В своем проекте мы решили создать единый объект, в котором ключи имеют полное соответствие строковому пути, кроме двух служебных:

  • index для корневых путей;

  • item для динамических путей элементов, обычно по идентификаторам.

Теперь все маршруты хранятся в таком виде:

export const Routes = {   index: '/',   login: '/login',   notFound: '/404',   search: '/search',   tasks: {     index: '/tasks',     actual: '/tasks/actual',     finished: '/tasks/finished',     item: (id: number, type: string) => `/tasks/${id}?type=${type}`   }, }

А навигация выглядит так:

const navigate = useNavigate(); ... navigate(Routes.tasks.item(123))

Это довольно простое и быстрое решение, и теперь у нас:

  • единая точка выбора пути, а автокомплит IDE быстро поможет найти нужный;

  • все пути чётко структурированы, легко ориентироваться и добавлять новые;

  • нет дублирования лишних слов в нейминге констант.

Не обязательно следовать именно нашей структуре: соблюдать точный нейминг ключей, юзать служебные index/item или единый объект — это лишь договорённость наших разработчиков. Главное — выбрать удобную и понятную схему для вашей команды.

Параметризуем маршруты

Но это было только начало. После этого мы решили раскрыть потенциал TypeScript.

Идея в том, чтобы связать параметры страниц и объект путей Routes.

Есть два типа параметров:

  • Path: /example/:id

  • Query: /example?id=123

Давайте сперва подготовим прототип получения параметров на страницах. Было бы классно просто передать в специальную утилиту наш путь или его тип из Routes, а в ответ получить параметры. Тогда все типы мы смогли бы описать единоразово в Routes. На примере React-хука:

const { id, type } = usePageParams<typeof Routes.tasks.item>();

Разберем пошагово, как этого добиться:

1. Сперва параметризуем пути в Routes.

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

export const Routes = {   ...   tasks: {     item: (id: number, type: string) => `/tasks/${id}?type=${type}`   } }

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

  • будет принимать путь в качестве аргумента (если path-параметров нет — строку, а если есть — функцию).

  • будет возвращать функцию генерации пути с внедрением path и query-параметров:

Также для удобства разделим path и query на разные объекты и введём простенький хелпер createUrl, который будет склеивать ориджин, путь, квери и хэш в итоговый URL.

import { stringify } from 'qs';  export type TParametrizedRoute<TPath = any, TQuery = any> = (params: {   path?: TPath;   query?: TQuery; }) => string;  // Утилита генератор функций: // - принимаем путь в виде строки или функции // - возвращаем функцию, которая конструирует маршрут страницы с параметрами function parametrizedRoute<TPath = undefined, TQuery = undefined>(   pathOrGetPath: TPath extends undefined ? string : (path: TPath) => string ): TParametrizedRoute<TPath, TQuery> {   return ({ path, query }: { path?: TPath; query?: TQuery }) => {     return createUrl({       path: typeof pathOrGetPath === 'function'               ? pathOrGetPath(path!)               : pathOrGetPath || '',       query     });   }; }  // В статье приводится примитивная универсальная утилита для склейки частей URLа. // Пример готовой библиотеки: https://github.com/meabed/build-url-ts export const createUrl = (args: {   origin?: string;   path?: string;   query?: Record<string, unknown>;   hash?: string; }): string => {   return [     args.origin || '',     args.path || '',     args.query ? `?${stringify(args.query)}` : '',     args.hash ? `#${args.hash}` : ''   ].join(''); };

Используем новую утилиту для создания путей, есть четыре возможных сценария:

export const Routes = {   tasks: {     // у страницы есть только query - передаём строку пути:     index: parametrizedRoute<undefined, { userId?: number; type?: string }>(       '/tasks'     )      // у страницы есть только path:     item: parametrizedRoute<{ id: number }>((params) => {       return `/tasks/${params.id}`;     }),      // у страницы есть и path, и query:     item: parametrizedRoute<{ id: number }, { type?: string }>((params) => {       return `/tasks/${params.id}`;     }),      // у страницы нет ни query, ни path:     list: '/tasks/list'   } };

2. Теперь напишем типы для извлечения параметров:

// Тип параметризованного пути уже ввели ранее:  export type TParametrizedRoute<TPath = any, TQuery = any> = (params: {   path?: TPath;   query?: TQuery; }) => string;  // Поскольку все конечные значения параметров в URL'е превратятся в строки, то лучше привести к ним, для этого используем специальный тип-конвертер: export type TObjectFieldsToStrings<T> = {   [K in keyof T]: T[K] extends string | undefined ? T[K] : string; };   // Утилита извлечет и преобразует нужное поле из аргумента параметризованного пути type TExtractRouteParams<   T extends TParametrizedRoute,   K extends keyof Parameters<T>[0] > = TObjectFieldsToStrings<   NonNullable<Parameters<T>[0][K]> >;  // Утилита извлекает path из параметризованного пути export type TExtractPageParams<T extends TParametrizedRoute>   = TExtractRouteParams<T, 'path'>;  // Утилита извлекает query из параметризованного пути export type TExtractPageQuery<T extends TParametrizedRoute>   = TExtractRouteParams<T, 'query'>;

3. И утилиты для извлечения параметров:

import { stringify } from 'qs'; import { useCallback, useMemo } from 'react'; import { useParams } from 'react-router'; import { URLSearchParamsInit, useSearchParams } from 'react-router-dom';  /**  * Утилита для извлечения параметров страницы (из path + из query)  */ export function usePageParams<T extends TParametrizedRoute>(): {   path: TExtractPageParams<T>;   query: Partial<TExtractPageQuery<T>>;   setParams: ReturnType<typeof useQueryParams<TExtractPageQuery<T>>>[1]; } {   const path = useParams() as TExtractPageParams<T>;   const [query, setParams] = useQueryParams<TExtractPageQuery<T>>();    return { path, query, setParams }; }   /**  * Утилита для работы с query-параметрами, если её нет во фреймворке  */ export function useQueryParams<T = Record<string, unknown>>(   defaultValues?: URLSearchParamsInit ): [Partial<T>, (values: Partial<T>, replace?: boolean) => void] {   const [value, setValue] = useSearchParams(defaultValues);    const parsedQueryParams = useMemo(     () => Object.fromEntries(value.entries()) as Partial<T>,     [value]   );    const setQueryParams = useCallback(     (values: Partial<T>, replace: boolean = true) => {       setValue(stringify({ ...parsedQueryParams, ...values }), { replace });     },     [parsedQueryParams]   );    return [parsedQueryParams, setQueryParams]; }

4. Итоговое получение параметров на странице будет выглядеть так:

export const TaskPage = (): ReactElement => {   const params = usePageParams<typeof Routes.tasks.item>();   const { path: { id }, query: { type } } = params;    return ( ... ); }

Готово! А автокомплит в IDE успешно подсказывает существующие параметры при их извлечении.

Улучшаем схему: автоматическое извлечение параметров из строк

А что, если избежать ручного указания path-параметров? Ведь обычно они уже содержатся в пути страницы. И этот же строковый путь передаётся в Router:

{   path: '/tasks/:id',   element: <TaskPage /> }

Для начала напишем упрощённую версию. Мы хотим передать строку пути и автоматически извлечь из неё параметры. Воспользуемся мощными возможностями языка TypeScript: используем infer и Template Literal Types.

/**  * Тип, который разбирает строку и извлекает параметры  */ type TExtractParams<T extends string> =   // Первый случай: строка содержит `:param/что-то`   T extends `${string}:${infer Param}/${infer Rest}`     // записываем тип "строка" по имени параметра     // и рекурсивно обрабатываем оставшуюся часть строки     ? { [K in Param]: string } & TExtractParams<`/${Rest}`>     : // Второй случай: строка содержит `:param` без слэшей       T extends `${string}:${infer Param}`       ? { [K in Param]: string }       : unknown;  const example: TExtractParams2<'/tasks/:first/:second'> = {   first: '123', // Автокомплит IDE работает   second: '456', }; 

Теперь нам нужно доработать уже известный parametrizedRoute, чтобы он принимал путь в качестве параметра и возвращал всё так же функцию для генерации пути. Для этого воспользуемся магией литералов в TypeScript.

А если формально, то используем вывод типа из строкового литерала с помощью конструкции <T extends string>(route: T).

const Routes = {   tasks: {     item: parametrizedRoute('/tasks/:id')   } };  // Мы не задаём тип вручную через generic, а наоборот: // он сам извлекается из аргумента, и мы можем использовать его далее function parametrizedRoute<T extends string>(route: T): TParametrizedRoute<T> {   const preparePath = (params: TExtractParams<T>): string => {     // TODO: замена параметров в пути на реальные значения (сделаем далее)     return '';   };   return ({ path }: { path: TExtractParams<T> }): string => {     return createUrl({ path: preparePath(path) });   }; }  // Проверяем: navigate(   Routes.tasks.item({     path: {       id: '123' // Автокомплит работает     }   }) );

Но здесь есть важный нюанс: нам ведь нужны ещё и query-параметры.

А если мы добавим их вторым аргументом, то магия литералов выключится и будем вынуждены пробрасывать путь в качестве дженерика, вот так:

function parametrizedRoute<P extends string, Q extends Record<string, unknown>>(   path: P,   query: Q ): TParametrizedRoute<P, Q> {   ... }  const Routes = {   tasks: {                   ↓↓↓ дублирование                         item: parametrizedRoute<'/tasks/:id', { type: string }>('/tasks/:id')   } };

Но такое дублирование кода нам совсем ни к чему.

Тогда нужно оставить у parametrizedRoute единственный generic-тип. А query привяжем с помощью цепочки вызовов там, где это нужно:

const Routes = {   tasks: {     // пример без query:     itemNoQuery: parametrizedRoute('/tasks/:id'),     // пример с query:     itemWithQuery: parametrizedRoute('/tasks/:id').withQuery<{       type: number;     }>()   } };

Доработаем утилиты и посмотрим на весь код:

/**  * Тип, который разбирает строку и извлекает параметры  */ export type TExtractParams<T extends string>   = T extends `${string}:${infer Param}/${infer Rest}`   ? { [K in Param]: string } & TExtractParams<`/${Rest}`>   : T extends `${string}:${infer Param}`     ? { [K in Param]: string }     : unknown;  /**  * Итоговый тип параметризированного пути:  * - функция с аргументом path-параметров.  * - поле-функция withQuery, которая возвращает функцию с аргументами path и query-параметров.  * - поле route в каждой функции с исходным роутом (например, для React Router'а)  */ type TParametrizedRoute<T extends string> = {   ({ path }: { path: TExtractParams<T> }): string;   route: string;    withQuery<TQuery extends Record<string, unknown>>(): {     ({ path, query }: { path: TExtractParams<T>; query: TQuery }): string;     route: string;   }; };  /**  * Утилита для создания параметризованного пути.  * Значения path и query-параметров автоматически подставляются  * в путь страницы при конечном вызове  */ function parametrizedRoute<T extends string>(route: T): TParametrizedRoute<T> {   const preparePath = (params: TExtractParams<T>): string => {     let processed: string = route;     Object.keys(params).forEach((key) => {       const value = params[key];       if (typeof value === 'string') {         processed = processed.replace(`:${key}`, value);       }     });     return processed;   };    const result = ({ path }: { path: TExtractParams<T> }): string => {     return createUrl({ path: preparePath(path) });   };   result.route = route;    result.withQuery = <TQuery extends Record<string, unknown>>() => {     const fn = ({ path, query }: { path: TExtractParams<T>; query: TQuery }): string => {       return createUrl({ path: preparePath(path), query });     };     fn.route = route;     return fn;   };    return result; }  /**  * Утилита для извлечения параметров страницы (из path + из query)  */ function usePageParams<T extends (args: any) => string>(_route: T) {   type RouteParams = Parameters<T>[0];   type P = RouteParams extends { path: infer Path } ? Path : never;   type Q = RouteParams extends { query: infer Query } ? Query : never;    const path = useParams() as P;   const [query, setParams] = useQueryParams<Q>();    return { path, query, setParams }; }

Обратите внимание, что использование usePageParams изменилось, теперь мы будем передавать роут не через тип, а в аргументы:

const {   path: { taskId },   query: { type }, } = usePageParams(Routes.tasks.item);
Проверяем, что всё работает и IDE подсказывает типы правильно:
export const useExample = (): void => {   const navigate = useNavigate();    // ВСЁ ОК   navigate(     Routes.tasks.itemWithQuery({       path: {         id: '123'       },       query: {         type: 456       }     })   );   // ВСЁ ОК   const withQuery = usePageParams(Routes.tasks.itemWithQuery);   console.log(withQuery.path.id); // '123' - string!   console.log(withQuery.query.type); // 456   console.log(withQuery.route); // 'tasks/:id'    // ВСЁ ОК   navigate(     Routes.tasks.itemNoQuery({       path: {         id: 123       }     })   );   // ВСЁ ОК   const noQuery = usePageParams(Routes.tasks.itemNoQuery);   console.log(noQuery.path.id); // 123 - number!   console.log(noQuery.route); // 'tasks/:id' };

С помощью полей «route» объект Routes можно использовать как единый источник правды при создании роутера. Это исключает расхождения и упрощает поддержку:

export const browserRouter = createBrowserRouter([   {     children: [       {         path: Routes.tasks.index.route,         element: <div>INDEX</div>       },       {         path: Routes.tasks.item.route,         element: <div>ITEM</div>       }     ]   } ]); 

Хм, а ЕЩЁ улучшить можно как-то?

Типизируем автоматические path-параметры

Не всегда path-параметры — это только строки или только числа. Давайте сделаем так, чтобы можно было указывать тип параметров:

const Routes = {   tasks: {     itemString: parametrizedRoute('/tasks/:id<string>'),     itemNumber: parametrizedRoute('/tasks/:id<number>'),     itemCustom: parametrizedRoute('/tasks/:id<custom>').withQuery<...>(),   } };

«А что за custom такой?», — спросите вы. Это мы так внедряем гибкость возможных типов: например, для поддержки строковых литеральных объединений, когда параметр может принимать только одно из фиксированных значений.

И ещё обработаем корнер-кейс, когда есть только query-параметры. Для этого напишем утилиту TRemoveEmptyObjects, которая преобразует пустой объект path: {} на необязательное поле path?: never.

parametrizedRoute('/example').withQuery<{ test: number }>()  …  type TRemoveEmptyObjects<T> = {   [K in keyof T as keyof T[K] extends never ? never : K]: T[K]; }; type TPathParams<T extends string> = TRemoveEmptyObjects<{ path: TExtractParams<T> }>; 

Новый план такой:

  • Парсим строку роута:

    • извлекаем path-параметры.из “скобочек” <…>

    • извлекаем строковый тип: “string” / “number” / “custom” / … .

  • Преобразовываем строковый тип в настоящий TS-тип: string / number / “a” | “b” / … .

  • На JS подставляем path-параметры (/:id), преобразуя в указанный тип.

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

Итоговый код с комментариями:

/*  * Мапы преобразования строки в фактический тип  */ type TDefaultType = number; type TParamTypeMap = {   string: string;   number: number;   custom: 'a' | 'b'; }; type TTypeConverters = {   [K in keyof TParamTypeMap]: (v: string | undefined) => TParamTypeMap[K] | undefined; }; export const TYPE_CONVERTERS: TTypeConverters = {   string: (v) => String(v),   number: (v) => (isNaN(Number(v)) ? undefined : Number(v)),   custom: (v) => (v === 'a' ? 'a' : 'b'), };  export const getParamRegExp = (key: string): RegExp => new RegExp(`:${key}(?:<([\\w]+)>)?`);  /**  * Тип, который скопирует переданный тип и тем самым объединит объекты в один  * {id: number} & {type: string} => { id: number; type: string }.  * Это визуально улучшит ошибки в IDE  * @see https://www.totaltypescript.com/concepts/the-prettify-helper  */ type TPrettify<T> = {   [K in keyof T]: T[K]; } & {};  type TPrettifyTests = [   Expect<     Equals<       TPrettify<{ first: string } & { second: { third: number } }>,       { first: string; second: { third: number } }     >   >, ];  /**  * Тип, который определяет тип path-параметра  */ type TGetParamType<Param extends string> =   // Проверяем, содержит ли строка имя и тип   Param extends `${infer NamePart}<${infer TypePart}>`     ? // Проверяем, указан ли такой тип в мапе       TypePart extends keyof TParamTypeMap       ? // Тип указан в мапе -> берём тип из мапы         { [K in NamePart]: TParamTypeMap[TypePart] }       : // Тип не указан в мапе -> дефолтный         { [K in NamePart]: TDefaultType }     : // Тип "<type>" не указан -> дефолтный       { [K in Param]: TDefaultType };  type TGetParamTypeTests = [   Expect<Equals<TGetParamType<'param<string>'>, { param: string }>>,   Expect<Equals<TGetParamType<'param<number>'>, { param: number }>>,   Expect<Equals<TGetParamType<'param<custom>'>, { param: 'a' | 'b' }>>, ];  /**  * Тип, который разбирает строку и извлекает параметры с учётом <type>  */ export type TExtractParams<T extends string> = TPrettify<   T extends `${string}:${infer Param}/${infer Rest}`     ? TGetParamType<Param> & TExtractParams<`/${Rest}`>     : T extends `${string}:${infer Param}`       ? TGetParamType<Param>       : unknown >;  type TExtractParamsTests = [   Expect<Equals<TExtractParams<'/:first'>, { first: TDefaultType }>>,   Expect<Equals<TExtractParams<'/start/:first<string>/end'>, { first: string }>>,   Expect<     Equals<       TExtractParams<'/start/:first<number>/middle/:second/end'>,       { first: number; second: TDefaultType }     >   >,   Expect<     Equals<       TExtractParams<'/:first<number>/:second/:third<custom>'>,       { first: number; second: TDefaultType; third: 'a' | 'b' }     >   >, ];  /**  * Обрабатываем случай, когда только квери: parametrizedRoute('/example').withQuery<{ test: number }>(),  * Пустой объект { path: {} } будет преобразован в необязательное поле { path?: never }  */ type TRemoveEmptyObjects<T> = {   [K in keyof T as keyof T[K] extends never ? never : K]: T[K]; };  type TRemoveEmptyObjectsTests = [   Expect<     Equals<       TRemoveEmptyObjects<{ first: {}; second: { example: string }; third: {} }>,       { second: { example: string } }     >   >, ];  /**  * Тип, который извлекает и преобразовывает path-параметры при их наличии  */ type TPathParams<T extends string> = TRemoveEmptyObjects<{ path: TExtractParams<T> }>;  type TPathParamsTests = [   Expect<Equals<TPathParams<'/:first<string>'>, { path: { first: string } }>>,   Expect<Equals<TPathParams<'/example'>, {}>>, ];  /**  * Тип, который преобразует все НЕ строки в строки. Нужен для Query.  */ type TObjectFieldsToStrings<T> = {   [K in keyof T]: T[K] extends string | undefined ? T[K] : string; };  type TObjectFieldsToStringsTests = [   Expect<     Equals<       TObjectFieldsToStrings<{ first: number; second: boolean; third: string }>,       { first: string; second: string; third: string }     >   >, ];  /**  * Итоговый тип параметризированного пути:  */ export type TParametrizedRoute<T extends string> = {   // здесь теперь TPathParams, который убирает пустой path   (params: TPathParams<T>): string;   // исходный путь вида /example/:id<number>/inner   initialRoute: T;   // путь для роутера вида /example/:id/inner   route: string;    withQuery<TQuery extends Record<string, unknown>>(): {     // здесь теперь TPathParams, который убирает пустой path     (params: TPathParams<T> & { query: TQuery }): string;     initialRoute: T;     route: string;   }; };  export function parametrizedRoute<T extends string>(route: T): TParametrizedRoute<T> {   const preparePath = (params: TPathParams<T>): string => {     if (!('path' in params)) {       return route;     }     let processed: string = route;     const path = params.path as TExtractParams<T>;     Object.keys(path).forEach((key) => {       processed = processed.replace(getParamRegExp(key), String(path[key]));     });     return processed;   };    function prepareRoute(value: string): string {     return value.replace(/:(\w+)<[^>]+>/g, ':$1');   }    const result = (params: TPathParams<T>): string => {     return createUrl({ path: preparePath(params) });   };   result.initialRoute = route;   result.route = prepareRoute(route);    result.withQuery = <TQuery extends Record<string, unknown>>() => {     const fn = (params: TPathParams<T> & { query: TQuery }): string => {       return createUrl({ path: preparePath(params), query: params.query });     };     fn.initialRoute = route;     fn.route = prepareRoute(route);     return fn;   };    return result; }  /**  * Утилита для извлечения параметров страницы (из path + из query)  */ export function usePageParams<   Route extends string,   Fn extends ((args: any) => string) & { initialRoute: Route }, >(fn: Fn) {   type RouteParams = Parameters<Fn>[0];   type P = RouteParams extends { path: infer Path } ? Path : never;   // Квери параметры возвращаются именно строками - преобразуем:   type Q = RouteParams extends { query: infer Query } ? TObjectFieldsToStrings<Query> : never;    const rawPathParams = useParams();   /**    * Преобразовываем значения параметров к ожидаемым типам    */   const path = useMemo(() => {     return Object.fromEntries(       Object.keys(rawPathParams).map((key) => {         const value = rawPathParams[key];         const match = fn.initialRoute.match(getParamRegExp(key));         const type = match?.[1];         const defaultConverter: (v: any) => TDefaultType | undefined = TYPE_CONVERTERS['number'];         const converter: (v: string | undefined) => any =           (type && TYPE_CONVERTERS[type]) ?? defaultConverter;         return [key, converter?.(value)];       }),     ) as P;   }, [rawPathParams, fn.initialRoute]);    const [query, setQuery] = useQueryParams<Q>();    return { path, query, setQuery }; }

Итоги

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

И это лишь один из возможных подходов — адаптируйте его под конкретные задачи и стиль вашей команды. Спасибо, что дочитали до конца!


ссылка на оригинал статьи https://habr.com/ru/articles/924208/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *