TypeScript

Typesafe Routing with React-Router: Advanced TypeScript

Aug 25, 2024
10 min read
Typesafe Routing with React-Router: Advanced TypeScript

# Typesafe Routing with React-Router: Advanced TypeScript

We use URLs in our code 1) to define routes and 2) to redirect to them. If we only had URLs without any dynamic parameters, then we could, in-theory, just store them as constants.

The route definition (ex: `/users/:userId`) is not dynamic, and therefore can be stored as a constant. When redirecting, we just need to build the URL from the definition by replacing the parameters with provided values, in a type-safe way.

## Defining Constants

```javascript
// src/constants.ts
export const Routes = {
USERS: "/users",
USER_DETAILS: "/users/:userId",
} as const;

// Utility type
export type Routes = (typeof Routes)[keyof typeof Routes];
```

Notice the use of `as const` here. The constant assertion is used to ensure that the object is inferred "literally". This means that the `type Routes` will be equal to `"/users" | "/users/:userId"` instead of `string`.

## Introducing `buildPath`

Here's what `buildPath` does:

```javascript
const url = buildPath("/users/:userId", { userId: "1" }); // Ok -> url = "/users/1"
const url = buildPath("/users/:userId", { id: "1" }); // TypeError: expected { userId: string }
const url = buildPath("/users"); // Ok -> url = "/users"
```

Ignoring the type-safe aspect, here's one way to implement it:

```javascript
function buildPath(path: string, params?: Record) {
return path.replace(/:([^/]+)/g, (_, key) => {
if (params[key] === undefined) {
throw new Error(`Missing parameter: ${key}`);
}
return params[key];
});
}
```

## Sprinkling TypeScript Magic ✨

This is where we get to have fun. We need a type that gives us the parameters as union of literals:

```typescript
type PathParam = Path extends `${infer L}/${infer R}`
? PathParam | PathParam
: Path extends `:${infer Param}`
? Param
: never;
```

```typescript
PathParam<"/a/:b"> = "b";
PathParam<"/:a/:b"> = "a" | "b";
PathParam<"/a"> = never;
```

We are essentially performing a divide-and-conquer algorithm using pattern matching!

## Almost There

Let's use `PathParam` with `buildPath`:

```typescript
type Params = { [key in PathParam]?: string };

function buildPath

(path: P, params?: Params

): string {
return path.replace(/:([^/]+)/g, (_, key) => {
if (params[key] === undefined) {
throw new Error(`Missing parameter: ${key}`);
}
return params[key];
});
}
```

## Enter "react-router-dom"

The good news: all the hardwork above is already done by the `generatePath` function from "react-router-dom":

```typescript
import { type Routes } from "./constants";
import { generatePath } from "react-router-dom";

function buildPath(...args: Parameters>): string {
return generatePath(...args);
}
```

## Conclusion

We now have a type-safe way of building paths to navigate to pages:

```typescript
import { useNavigate } from "react-router-dom";
import { buildPath } from "./utils";

export const SomePage = () => {
const navigate = useNavigate();

function redirect() {
navigate(buildPath(Routes.USER_DETAILS, { userId: "1" }));
}

return ;
};
```

TypeScript is amazing for catching routing errors at compile time!

Enjoyed this article?

Follow me on social media for more insights on web development and technology.