Useful types: Build a route tree with TypeScript
Andrei Kondratev
Posted on June 21, 2022
Hello there!
In my previous article I wrote how to extract the type of route's params. Look there 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, so we can define something like this
<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 and generics for this. Let's define a type
type SomeData<Other> = {
path: string;
} & Other;
So, SomeData
defines the path
property and also other properties from Other
.
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 that creates the definition object and automatically infers its type.
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
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 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
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.
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
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.
Posted on June 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.