Home > Web Front-end > JS Tutorial > Eliminate Runtime Errors with Type-safe Routes in Angular

Eliminate Runtime Errors with Type-safe Routes in Angular

Patricia Arquette
Release: 2025-01-06 07:22:40
Original
224 people have browsed it

Eliminate Runtime Errors with Type-safe Routes in Angular

Refactoring an Angular application can be a double-edged sword. On one hand, it allows you to improve the maintainability and scalability of your codebase. On the other hand, it can lead to broken routes if you haven't taken the necessary precautions to protect your features against unintended changes. Writing extensive tests or implementing a solid typing concept for routes can help mitigate this risk, but these approaches can be time-consuming and may not always be feasible. In this article, we'll explore a more efficient solution that automatically detects broken routes in compile-time, without requiring manual test efforts or the need to write custom type annotations. We'll demonstrate this approach by implementing a sample Angular application with nested components and using the typesafe-routes library to improve developer experience and facilitate parameter parsing.

To illustrate the benefits of automatically detecting broken routes in compile-time, we'll implement a sample Angular application with three nested components: DashboardComponent (/dashboard), OrgsComponent (/orgs/:orgId), and LocationsComponent (/orgs/:orgId/locations/:locationId). To set up this example, we'll need to install the typesafe-routes library and use its createRoutes function to define our route tree, as shown in the following code fragment.

// app.routes.ts
import { createRoutes, int } from "typesafe-routes";

export const r = createRoutes({
  dashboard: {
    path: ["dashboard"], // ~> "/dashboard"
  },
  orgs: {
    path: ["orgs", int("orgId")], // ~> "/orgs/:orgId"
    children: {
      locations: {
        path: ["locations", int("locationId")], // ~> "locations/:locationId"
        query: [int.optional("page")], // ~> "?page=[number]"
      },
    },
  },
});
Copy after login
Copy after login
Copy after login

Let's take a closer look at the code fragment. We import createRoutes from typesafe-routes and pass on our routes as its first argument. These routes are defined as a nested object with two properties at the root level: dashboard and orgs. Each of these properties is assigned a path, specifying the segments in the form of an array. For example, the ["dashboard"] array corresponds to the path /dashboard. The orgs path is more complex, as it contains a parameter named orgId of type integer. Note that integer is not a native JavaScript type, but rather a custom type defined using the int function, which mimics the characteristics of an integer using a number in the background. The orgs route has a children property, which specifies one child route called locations. The locations route is similar to the orgs route, but it specifies an additional optional search parameter page of type int.

createRoutes uses the information about the routes to create a context wrapped in a Proxy object. You don't need to know the details about that proxy object, but it's essential to understand that thanks to that object, you can access all routes specifications anywhere in your application to render and parse routes and parameters.

We assigned the Proxy object returned by createRoutes to r. This means you can access the dashboard path with r.dashboard, locations path with r.orgs.locations, and so on.

Rendering Templates

With our routes defined, we can now move on to the next step: registering them with angular-router.

// app.routes.ts
import { createRoutes, int } from "typesafe-routes";

export const r = createRoutes({
  dashboard: {
    path: ["dashboard"], // ~> "/dashboard"
  },
  orgs: {
    path: ["orgs", int("orgId")], // ~> "/orgs/:orgId"
    children: {
      locations: {
        path: ["locations", int("locationId")], // ~> "locations/:locationId"
        query: [int.optional("page")], // ~> "?page=[number]"
      },
    },
  },
});
Copy after login
Copy after login
Copy after login

The code fragment shows a common setup with nested routes for Angular Router that mirrors the route tree we defined earlier. However, instead of using typical plain strings to specify the path templates (for instance orgs/:orgId), we import the template function from typesafe-routes/angular-router and use it to generate the path templates. For the DashboardComponent and OrgsComponent, we can simply call template with their corresponding paths r.dashboard and r.orgs to get the templates. However, the remaining component LocationsComponent is a child of OrgsComponent and thus requires a relative path, which cannot be generated by using r.orgs.locations as this would result in an absolute path orgs/:orgId/locations/:locationId, whereas Angular Router expects a relative path when nesting route templates.

To generate a relative path, we can use the _ link, which effectively omits everything that precedes the underscore character. In this case, we can use template(r.orgs._.locations) to generate the relative path. This is a handy feature, as it allows us to reuse the same route tree in scenarios where we need to render absolute paths but also in situations that require a relative path.

At this point we already took advantage of autocompletion and typo prevention in our favourite IDE (such as Visual Studio Code). And future changes will alert us to any misspelling or typos in our route paths because all types can be traced back to the initial routes definition with createRoutes.

Rendering Links

Now that we have specified our route templates, we want to move on to link rendering. For that, we want to create a simple component that utilizes render functions to render those links, including type serialization and type checks. The next example shows a component that renders a list of anchor elements referencing other components in our application.

// app.routes.ts
import { Routes } from "@angular/router";
import { template } from "typesafe-routes/angular-router";

export const routes: Routes = [
  {
    path: template(r.dashboard), // ~> "dashboard"
    component: DashboardComponent,
  },
  {
    path: template(r.orgs), // ~> "orgs/:orgId"
    component: OrgsComponent,
    children: [
      {
        path: template(r.orgs._.locations), // ~> "locations/:locationId"
        component: LocationsComponent,
      },
    ],
  },
];
Copy after login
Copy after login

The code example imports render and renderPath from typesafe-routes/angular-router. renderPath renders a path, whereas render additionally serializes the query parameters for our link list. We also import r, the proxy object that allows us to access the information about the previously defined routes and to define the desired route to be rendered.

First, we create dashboardLink and orgsLink using the renderPath function. As the first parameter, it takes the aforementioned proxy object representing the path of the route to be rendered. The second parameter is a record with parameter values matching the name and type of the parameter previously defined with createRoutes in app.routes.ts. The return value is a string containing the path belonging to the corresponding component.

The render function in the third example renders both path and search parameters, and thus requires a path and a query property in the parameter definitions. The return value here is an object with the two properties path and query. We set the two properties as the values of the [routerLink] and [queryParams] attributes.

Parsing Parameters

Parameter parsing is an essential part of typesafe-routes. During route definition above, we defined a couple of parameters and gave them an integer-like type int. However, since the parameter values come from various sources such as the Location object, they are string-based. Conveniently, typesafe-routes exports helper functions that parse these strings and cast them to the desired type. Parsing is based on our proxy object r we created earlier, meaning we have to tell the library what route the params belong to. The next example demonstrates that by showing two common parsing scenarios.

// app.routes.ts
import { createRoutes, int } from "typesafe-routes";

export const r = createRoutes({
  dashboard: {
    path: ["dashboard"], // ~> "/dashboard"
  },
  orgs: {
    path: ["orgs", int("orgId")], // ~> "/orgs/:orgId"
    children: {
      locations: {
        path: ["locations", int("locationId")], // ~> "locations/:locationId"
        query: [int.optional("page")], // ~> "?page=[number]"
      },
    },
  },
});
Copy after login
Copy after login
Copy after login

Given the location.href orgs/1/location/2?page=5, in Angular, we can access string-based query params using this.route.snapshot.queryParams and string-based path parameters are provided via this.route.snapshot.params. Using parseQuery with r.orgs.locations and this.route.snapshot.queryParams, we can retrieve an object with the page parameter as a number. Using parsePath with r.orgs._.locations and this.route.snapshot.params, we get the parsed locationId. In this case, r.orgs._.locations is a relative path, and all the segments before the _ link are omitted, causing orgId not to be present in the resulting object.

The parsing functions in typesafe-routes are versatile, and we can also extract all the parameters directly from the location.href string at once using parse.

// app.routes.ts
import { Routes } from "@angular/router";
import { template } from "typesafe-routes/angular-router";

export const routes: Routes = [
  {
    path: template(r.dashboard), // ~> "dashboard"
    component: DashboardComponent,
  },
  {
    path: template(r.orgs), // ~> "orgs/:orgId"
    component: OrgsComponent,
    children: [
      {
        path: template(r.orgs._.locations), // ~> "locations/:locationId"
        component: LocationsComponent,
      },
    ],
  },
];
Copy after login
Copy after login

Extracting type information about parameters is possible via InferQueryParams, InferPathParams, or InferParams. Here is a demonstration of the InferQueryParams utility type.

// app.component.ts
import { render, renderPath } from "typesafe-routes/angular-router";
import { r } from "./app.routes";

@Component({
  selector: "app-root",
  imports: [RouterOutlet, RouterLink],
  template: `
    <h1>Absolute Links</h1>
    <ul>
      <li><a [routerLink]="dashboardLink">Dashboard</a></li>
      <li><a [routerLink]="orgsLink">Org</a></li>
      <li>
        <a [routerLink]="locationLink.path" [queryParams]="locationLink.query">
          Location
        </a>
      </li>
    </ul>
    <router-outlet></router-outlet>
  `,
})
export class AppComponent {
  dashboardLink = renderPath(r.dashboard, {}); // ~> dashboard
  orgsLink = renderPath(r.orgs, { orgId: 123 }); // ~> orgs/123

  locationLink = render(r.orgs.locations, {
    path: { orgId: 321, locationId: 654 },
    query: { page: 42 },
  }); // ~> { path: "orgs/321/location/654", query: { page: "42" }}
}
// ...
Copy after login

Wrapping Up

To conclude this tutorial, we have created a single routes tree r that is the single source of truth for our routes. Based on that, we rendered templates that we used to register our components with Angular Router. We rendered paths with dynamic path segments and query parameters. We parsed parameters to convert them from string values to their corresponding types. We did everything in a type-safe manner without writing even one single type definition. We have established a robust routes tree that easily prevents bugs while developing new features and furthermore facilitates future refactorings.

However, typesafe-routes has many more features, such as many different built-in parameter types, easy integration of custom parameter types, manipulation of subpaths, define custom template strings, and many more. Unfortunately, we can't cover them all in this tutorial, but you can read more by visiting the official documentation.

Support the Project

Of course, there are also many potential improvements that can be implemented to the examples shown in this tutorial. For example, a custom directive for link rendering that takes on a path definition based on our proxy object, such as r.orgs.locations. Another example is a function that automatically generates a Routes array for Angular Router, effectively eliminating duplicated code and the need to keep the routes in sync with our route tree created with createRoutes in the very first code block.

However, these are just a few ways among many to contribute. The most common way is, of course, sharing, reporting bugs, or opening PRs in our GitHub repository. If you use this library and think it improves your development experience, you could also buy me a coffee. We also have a Discord channel where you can leave feedback or ask questions.

The above is the detailed content of Eliminate Runtime Errors with Type-safe Routes in Angular. For more information, please follow other related articles on the PHP Chinese website!

source:dev.to
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template