Persistent routes
Tags: frontend, typescript
Published: 2021-09-28 12:56:16

Hi, everyone. I'm sure you know, route management in an enterprise project is a tricky task. Enum (typescript data structure) is often used to keep them persistent. But enum itself doesn't solve all problems while we handle the route concatenation. Let's take a look at an example.

Suppose we have three levels of nesting routes:

```typescript
enum ROOT_ROUTES {
  CATALOG = 'catalog',
  ABOUT = 'about',
  SUPPORT = 'support',
}

enum CATALOG_ROUTES {
  CATEGORY = 'category',
  ITEM = 'item',
  CART = 'cart'
}

enum CATEGORY_ROUTES {
  TOP = 'top',
  SALES = 'sales',
  ALL = 'all'
}
```

Then, in order to assemble the full route, we have to correctly concatenate all the intermediate values:

```typescript
const topCategoryRoute = `/${ROOT_ROUTES.CATALOG}/${CATALOG_ROUTES.CATEGORY}/${CATEGORY_ROUTES.TOP}`;
```

It's a very bad idea to keep it just as it is. I'm sure you don't do that. It's easy to overlook any nesting level. And if similar code starts to appear in several places of project, we violate the DRY principle. It will also lead to problems in the future when we want to change the navigation.

The situation looks better when we add global constants with the routes already concatenated:

```typescript
const rootRoutes: { [key in keyof typeof ROOT_ROUTES]: string } = {
  CATALOG: `/${ROOT_ROUTES.CATALOG}`,
  ABOUT: `/${ROOT_ROUTES.ABOUT}`,
  SUPPORT: `/${ROOT_ROUTES.SUPPORT}`
}

const catalogRoutes: { [key in keyof typeof CATALOG_ROUTES]: string } = {
  CATEGORY: `${rootRoutes.CATALOG}/${CATALOG_ROUTES.CATEGORY}`,
  ITEM: `${rootRoutes.CATALOG}/${CATALOG_ROUTES.ITEM}`,
  CART: `${rootRoutes.CATALOG}/${CATALOG_ROUTES.CART}`
}

// It's terribly tedious even just to write this example.
// Let's move on to the final part.
```

And even so, we are not immune to mistakes.

Let's try to automate the process to get rid of the routine and reduce the human factor. I started to think not from what I can do, but from what I want to get.

Suppose we already have `rootRoutes` with a signature corresponding to `ROOT_ROUTES`. I would like the `rootRoutes` properties to behave like primitive values in operations, but at the same time have a method for the subsequent concatenation of routes. Something like that (TDD in action):

```typescript
// https://nodejs.org/api/assert.html#assert_assert_strictequal_actual_expected_message
assert.strictEqual(`${rootRoutes.CATALOG}`, '/catalog')
const catalogRoutes = rootRoutes.CATALOG.concatRoutes(CATALOG_ROUTES);
assert.strictEqual(`${catalogRoutes.CATEGORY}`, '/catalog/category');
```

The implementation of the [`[Symbol.toPrimitive]` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive) in the class will help us achieve this.

Final result:
```typescript
class Route {
  constructor(private _value: string) {}

  [Symbol.toPrimitive]() {
    return this._value;
  }

  concatRoutes<R extends Record<string, string>, K extends keyof R>(routes: R) {
    return Object.fromEntries(
      Object.entries(routes).map(([key, value]) => [key, new Route(`${this._value}/${value}`)])
    ) as { [key in K]: Route };
  }
}

const rootRoutes = new Route('').concatRoutes(ROOT_ROUTES);
```

Thank you for reading. Please share, how do you manage routes in your project?