Getting Started

1. Install

pnpm add modernjs-typed-routes
# npm install modernjs-typed-routes

One package provides both the build-time plugin (modernjs-typed-routes/plugin) and the runtime wrappers (modernjs-typed-routes).

2. Register the plugin

modern.config.ts
import { appTools, defineConfig } from '@modern-js/app-tools';
import { routeTypesPlugin } from 'modernjs-typed-routes/plugin';

export default defineConfig({
  plugins: [
    appTools(),
    routeTypesPlugin(), // zero config
  ],
});

3. Generate

Types are generated automatically when you run modern dev or modern build, and regenerate whenever your routes change while the dev server runs — a route file is added, removed or renamed, or modern.routes.ts is edited. (Editing a page's content doesn't regenerate anything: the routes didn't change.) To generate without starting anything (e.g. CI):

npx modern typegen

You'll get a single generated file:

src/routes.gen.d.ts (generated)
/* eslint-disable */
/* prettier-ignore */
// AUTO-GENERATED by modernjs-typed-routes. DO NOT EDIT.
// Regenerate: run `modern dev`, `modern build`, or `npx modern typegen`.
export {};
declare module 'modernjs-typed-routes' {
  interface Register {
    routes: {
      '/': { params: {} };
      '/about': { params: {} };
      '/blog': { params: {} };
      '/blog/[id]': { params: { id: string | number } };
      '/checkout': { params: {} };
      '/docs/$': { params: { '*': string } };
      '/login': { params: {} };
      '/users/[id$]': { params: { id?: string | number } };
    };
  }
}

Because the file lives in src/, your existing tsconfig.json picks it up with no extra configuration — and you can always open it to see exactly what the plugin derived from your routes.

Commit or gitignore?

Both work. We recommend committing it at first (readable diffs whenever routes change, and CI typechecks without a generation step). Once you trust the tool, you can gitignore it and run npx modern typegen in CI instead.

4. Navigate with types

Import navigation APIs from modernjs-typed-routes instead of @modern-js/runtime/router — the package re-exports everything from the router, with Link, Navigate and useNavigate replaced by typed versions:

src/routes/blog/page.tsx
import { Link, useNavigate } from 'modernjs-typed-routes';

export default function BlogPage() {
  const { navigateTo } = useNavigate();

  return (
    <div>
      {/* Paramless route: just a string — autocompleted */}
      <Link to="/about">About</Link>

      {/* Dynamic route: `params` is required and typed */}
      <Link to="/blog/[id]" params={{ id: 42 }}>
        Read post 42
      </Link>

      <button onClick={() => navigateTo('/blog/[id]', { params: { id: 42 } })}>
        Or imperatively
      </button>
    </div>
  );
}

That's the whole setup. See Navigation for the full API.

CI recipe

Catch broken links in pull requests:

package.json
{
  "scripts": {
    "typecheck": "modern typegen && tsc --noEmit"
  }
}

Troubleshooting

  • Types don't update in the editor — the file regenerated but the TS server cached it: run "TypeScript: Restart TS Server". This is rare; the dev server keeps the file current.
  • to accepts any string — the Register is empty because types were never generated. Run npx modern typegen and confirm src/routes.gen.d.ts exists and is inside your tsconfig include.

More cases (multi-entry, monorepos, custom basename, lint tools) in the full Troubleshooting guide.