Useful types: Build a route tree with TypeScript
Tags: react, typescript, tutorial, routing
Published: 2022-06-21 08:40:08

Hello there!

In my previous article I wrote how to extract the type of route's params. Look [there](https://dev.to/bytimo/useful-types-extract-route-params-with-typescript-5fhn) if you haven't seen it yet. Now I'm going to describe how I create route definitions in my project.

First of all, let's define some terms that will be used next. `react-router-dom@6` allows to use [nested routes](https://reactrouter.com/docs/en/v6/getting-started/tutorial#nested-routes), so we can define something like this
```tsx
<Route path="/">
  <Route path="card" element={...}>
    <Route path=":cardId" element={...}>
      {...}
    </Route>
  </Route>
</Route>
```
In the code above `/`, `card` and `:cardId` are destructed segments of some path. If we join them, we get `/card/:cardId`. So let's call one of such segments a `path` and joined segments from some root to each specified path a `fullPath`.

OK, we need to use a `path` for some cases and a `fullPath` for another. And in my mind all data about every single route definition must be stored in one place. Moreover, this definition may have other information about the route, for instance, page title, default query-params, some flags or so on. And, of cause, I want to define route definitions as a tree because of the code above.

Briefly my ideal route definition:
- can be built as a tree
- stores all necessary data in the each node
- automatically infers strong types (yes it's necessary)
- is an once-declared structure that's shared throughout the application

First of all, let's see how to make a strongly typed tree. We can use [an intersection of object types](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types) and [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) for this. Let's define [a type](https://www.typescriptlang.org/play?#code/C4TwDgpgBAyg9gWwgEQIbFQHgPLABYQBOAfFALxQDeAUFFGOngFxQDOwhAlgHYDmA3NQC+UAGRRcBQoOoBjON3ZQAjgEYW8JGgyZKQ0hUoN8LAESmh1ADYRgKgEwbEKdFkqoW3AK4IARkX1yKmNmKHMAGigPKFUABksbO2UAZictV11uCHYIABM0lx09YkDDWnpGM1Nw8qyc-ODKsIthIA)

```typescript
type SomeData<Other> = {
  path: string;
} & Other;
```

So, `SomeData` defines the `path` property and also other properties from `Other`.

```typescript
const q1: SomeData<{}> = {path: ""}
let q2: SomeData<{a: number}> = {path: "", a: 10}
let q3: SomeData<{nested: SomeData<{}>}> = {
  path: "",
  nested: {path: ""}
}
```

This solution allows to define a tree-like type of our routing definition, but it requires to write types manually. So we can declare [some function](https://www.typescriptlang.org/play?ssl=18&ssc=38&pln=15&pc=1#code/C4TwDgpgBASg9gV2BAIhAZgSwHaeJubAHgDkIBnZAEwD4oBeKAbwCgoowBDYACwC4olAE44A5gG4WAXygAyKGUoQqklugTYAxvkJQhiZKQrUaACjYdu-QcBHZRAGgvZjygYuosAlAPhJUGDh4BMQeynSs7EIQwAhC2MwW7Fy8TuzsAHRZLkpUaVBS0iwsmoSUUACOAIwMegYQpgBEmpxCVI0OzFJe4lAA9H3MKdbNre2FpdjlFQBMtfr+TS1tHYnsnAILyE2cq0zd0j39g0zDAqMrnRtDVue7UlJAA) that creates the definition object and automatically infers its type.

```typescript
type RouteDefinition<Nested> = {
  path: string;
} & Nested;

function route<Nested>(
  path: string,
  nested: Nested
): RouteDefinition<Nested> {
  return {
    path,
    ...nested,
  }
}
```
In this case we can use the function `route` to create one routing definition node and then reuse the function for nested definitions

```typescript
const q1 = route("card", {}); // {path: "card"}
const q2 = route("card", {
  a: route("a", {})
}); // {path: "card", a: {path: "a"}}
```

Maybe now it looks like something not very convenient, but we'll return to this in the future.

What about the full path property? We define one part of the full path inside the definition node and all nested definitions must contain this prefix in their full path. I suggest [to change](https://www.typescriptlang.org/play?#code/C4TwDgpgBASg9gV2BAIhAZgSwHaeJubAHgDkIBnZAEwD4oBeKAbwCgoowBDYACwC4olAE44A5gG42UdAgA2sgArd+g4COwSWAXygAyKGUoQqkljOwBjfIShDEyUhWo0AFFK68BwsQBp3QjEwADy81XykLAO4IQ2oAfgEXGXklT1V1UQBKBjpY4z9MgXgkVEDca2I82mYIwkppOUVlBigAAwASJjAArCCtAHpOjx4tVtrseuwnYxbIiGiqqDioOYXpqiTG1J5sxKYdTnIDdczTdgDgBCFsGvZ2Yb87hpTlR7uAOk+poyo3rW0WCwLHVgFAAI4ARhadhKLgARBZOEIqHCfFA4XDTlB+v1mMMBAikSi0ckmmk4f1Eci4f8gSDwQAmaH2CDwqnE9Gojg9YI5KAuVjsTgCGHIeGcLndQJBTJvABGIpZ8Llkp5QTRUt6fIFUnYFkVsIRqulmSkWkypvNWJY-QAVLapIK7vj0ezUbrnmSVBS3W8hQInU8XXCJX67qTtgTKUT+hKPVowwrbk8OMoCSqw+wI2n0dHkf0Mx69QGi86c0bM+GtuW81QC5S4aX-k9m1Bm7b+kA) the `nested` object to the function that takes as the first parameter the full path for all nested routes and returns the nested route definitions.

First of all, we'll add the `fullPath` property to the `RouteDefinition` type

```typescript
type RouteDefinition<Nested> = {
  path: string;
  fullPath: string;
} & Nested;
```

Then we'll need to add the `prefix` parameter to the `route` function that it'll be possible to define the node with the nested full path. We'll also change the `nested` object to the `createNested` function that's been described above. Let's make it optional for a more convenient use.

```typescript
function route<Nested>(
  path: string,
  prefix: string,
  createNested?: (fullPath: string) => Nested,
): RouteDefinition<Nested> {
  const fullPath = `${prefix}/${path}`
  const nested = createNested 
    ? createNested(fullPath) 
    : ({} as Nested);

  return {
    path,
    fullPath,
    ...nested,
  }
}
```

And now we can use this function for defining nested routes like this

```typescript
const q1 = route("card", ""); // {path: "card", fullPath: "/card"}

const q2 = route("card", "", prefix => ({
  a: route("a", prefix),
  b: route("b", prefix, prefix => ({
    c: route("c", prefix)
  }))
})); 
/**
  {
    path: "card",
    fullPath: "/card",
    a: {
      path: "a",
      fullPath: "/card/a"
    },
    b: {
      path: "b",
      fullPath: "/card/b",
      c: {
        path: "c",
        fullPath: "/card/b/c"
      }
    }
  }
 */
```

We can create all route definitions as an object and share it throughout our application. You also can append another properties like `title`, `isModal`, etc to the node definition. Moreover, such approach can be used not only to create a route tree, but also to create any tree-like structures.

In the next article I would like to describe how to work with parameters in the url and build properties in the route definition that depend on the url parameter. Follow me and see you in the next article.