angular-typed-router
v0.1.1-4
Published
## Attention: Still in experimental phase
Downloads
14
Readme
angular-typed-router
Attention: Still in experimental phase
Type-safe ergonomic primitives on top of Angular's standalone router. Automatically derive a compile‑time union of valid route URL strings (Path) and strongly typed navigation command tuples (Commands) from the consumer application's Routes definition – without generating code or adding runtime weight.
Why
Angular's router is powerful but untyped for URL literals – a misspelled path or an outdated segment only fails at runtime. This library lets your application declare routes once, then:
- Navigate with
TypedRouter.navigateByUrl(path)wherepathis validated at compile time. - Use
<a routerLink="...">with type checking via an augmentedTypedRouterLinkdirective. - Build command tuples with correct literal segments (
Commandstype).
No decorators, no custom builders, no code generation – just TypeScript type inference and interface augmentation.
Installation
ng add angular-ryped-router will set up the package and create a declaration file for you.
Or install manually:
npm install angular-typed-router
# or
pnpm add angular-typed-router
# or
yarn add angular-typed-routerQuick Start
- Define your application routes:
// app.routes.ts
import { Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
export const appRoutes = [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'projects/:id', loadComponent: () => import('./project.component').then((m) => m.ProjectComponent) },
{ path: '**', redirectTo: 'dashboard' },
] as const satisfies Routes;- Create the augmentation file so the library can “see” your routes:
// typed-router.d.ts (sibling to main.ts or inside src/ root)
import type { appRoutes } from './app/app.routes';
declare module 'angular-typed-router' {
interface UserTypedRoutes {
routes: typeof appRoutes;
}
// Customize route param types here, the keys of this interface match your route param names
interface RouteParamTypes {
id: `${number}`;
// other params...
}
}- Link the augmentation file in your tsconfig so the compiler includes it:
// tsconfig.app.json
{
"extends": "./tsconfig.json",
"compilerOptions": {},
"include": [
"src/**/*.ts",
"typed-router.d.ts" // <— add this line
]
}If you have multiple tsconfigs, ensure the specific app tsconfig that drives the build/test includes the file.
- Use the typed router & link:
import { Component, inject } from '@angular/core';
import { TypedRouter, Path } from 'angular-typed-router';
@Component({
selector: 'app-nav',
template: `
<a routerLink="dashboard">Dashboard</a>
<a routerLink="projects/123">Project 123</a>
`,
})
export class NavComponent {
private readonly router = inject(TypedRouter);
go(p: Path) {
// p must be one of the inferred paths
this.router.navigateByUrl(p);
}
openProject(id: string) {
// commands tuple – first literal segment + dynamic param value
this.router.navigate(['projects', id]);
}
}If you try this.router.navigateByUrl('projcts/123') (typo) or <a routerLink="projcts/123">, TypeScript errors.
Exports
import { TypedRouter, TypedRouterLink, Path, Commands, UserTypedRoutes, RouteParamTypes } from 'angular-typed-router';TypedRouter– Extends AngularRouter, overridesnavigateByUrl&navigatesignatures to acceptPath/Commands.TypedRouterLink– Directive shadowing[routerLink]to type its input asCommands | Path.Path– Union of every reachable concrete URL path produced from your route tree (includes parameterized expansions withstringin place of:paramsegments).Commands– Union of tuple command arrays representing validRouter.navigate()inputs (each static segment as a literal, each parameter position asstring).UserTypedRoutes– Empty interface you augment with yourroutesreference.ExtractPathsFromRoutes<Routes>– Utility type if you need to compute from an arbitraryRoutesarray manually.RouteParamTypes– Interface you can augment to specify types for route parameters by name (e.g.id: ${number}).
How It Works
- You augment
UserTypedRouteswith the literalconstroute array. - Type utilities recursively walk the route tree (including lazily loaded routes via
loadChildrenreturningRoute[]or{ routes }). - Each navigable route (component / loadComponent / redirectTo) contributes a path string. Param segments (e.g.
:id) can be typed through declaration merging ofRouteParamTypes. - Child paths are joined with parents to form final concrete path unions.
- A tuple transformation creates the
Commandsvariants.
All compile-time only; nothing ships to runtime.
Lazy Routes
Works with any lazy route whose loadChildren resolves to:
Promise<Route[]>Promise<{ routes: Route[] }>(Angular v17+ pattern)
Example:
{ path: 'admin', loadChildren: () => import('./admin.routes').then(m => m.adminRoutes) }Those child paths get prefixed (admin/...) in Path & Commands.
Parameter Segments
A pattern projects/:id/details/:section produces a Path variant like:
'projects/' + IdParamType + '/details/' + SectionParamTypeand a Commands tuple like:
['projects', IdParamType, 'details', SectionParamType]You pass real runtime values for the IdParamType and SectionParamType positions. Empty string values and values like 'param/still-param' cannot currently be prevented at the type level without hurting DX (see Limitations).
Usage Patterns
Navigate by full path (typed):
router.navigateByUrl('/dashboard');Navigate with commands array:
router.navigate(['/', 'projects', someId]);Generate a UrlTree:
router.createUrlTree(['/', 'projects', id]);Template links:
<a routerLink="/projects/42">Project 42</a> <a [routerLink]="['/', 'projects', projectId]"></a>Augmentation Placement
Keep the augmentation in a .d.ts that is included by tsconfig.app.json (include array). If you see Path still as never, ensure:
- The augmentation file is included.
- The
routesconstant isas const satisfies Routes. - No circular import (augmentation file should only import the routes, nothing else runtime-heavy).
Limitations & Tradeoffs
| Concern | Status / Rationale |
| ---------------------------------- | ------------------------------------------------------------------------------------- |
| relativeTo (relative navigation) | Not supported – all inferred Path / Commands are absolute. Use absolute commands. |
ESLint Recommendation (Optional)
You can use angular-typed-router-eslint plugin to forbid untyped navigation calls.
Troubleshooting
| Symptom | Fix |
| --------------------- | -------------------------------------------------------------- |
| Path is never | Check augmentation file is included in tsconfig. |
| Lazy children missing | Ensure promise resolves to Route[] or { routes: Route[] }. |
Contributing
PRs welcome.
License
MIT
Happy routing.
