Introduction

The problem

Modern.js gives you excellent file-based routing on top of React Router v7 — but navigation is stringly-typed:

// Compiles fine. 404s at runtime.
navigate('/blog/postz/42');

// Renamed routes/blog → routes/articles? Every <Link> silently breaks.
<Link to="/blog/42" />

Next.js has typedRoutes, React Router has typegen, SvelteKit has $types, TanStack Router is typed end-to-end. Modern.js has… an open feature request.

The solution

modernjs-typed-routes is a Modern.js CLI plugin that reads the framework's own parsed route tree and generates a single declaration file. Combined with the typed wrappers it ships, you get:

import { Link, useNavigate } from 'modernjs-typed-routes';

// ✅ Autocomplete for every route in your app
<Link to="/about" />

// ✅ `params` is REQUIRED here — and `id` is typechecked
<Link to="/blog/[id]" params={{ id: post.id }} />

// ❌ Compile error: route does not exist
<Link to="/blgo/[id]" params={{ id: post.id }} />

// ❌ Compile error: missing required param `id`
const { navigateTo } = useNavigate();
navigateTo('/blog/[id]');

How it works

  1. During modern dev / modern build (or npx modern typegen), the plugin taps Modern.js's route generation and receives the fully-parsed route tree — including config routes from modern.routes.ts and every entry of a multi-entry app.
  2. It emits one file (default src/routes.gen.d.ts) containing only types: a map of every route path to its params, merged into the package's Register interface via declaration merging.
  3. The wrappers you import (Link, Navigate, useNavigate, …) are static generic code that infers everything from Register. No runtime code is ever generated, and your bundle stays the same.

Because the types come from Modern.js's own parser — not a re-implementation of its conventions — they can't drift from what the framework actually serves.

Comparison

modernjs-typed-routesNext.js typedRoutesReact Router typegenTanStack Router
FrameworkModern.jsNext.jsRR framework modeTanStack
Typed path union✅ (href())
Params required per route✅ objectstemplate strings only✅ (href())
Typed navigation components✅ ships wrappersnext/link built-inpartial✅ built-in
Search paramsbasic (validated planned)✅ validated
Bundle costnone (types only)nonenoneruntime router

Requirements & compatibility

Supported
Modern.jsv3@modern-js/app-tools / @modern-js/runtime ^3.6.0 (developed and tested against 3.6)
Node.js≥ 20 (Modern.js v3.6's own floor)
TypeScript≥ 5.3 — Modern.js 3.6's own toolchain types don't parse on 5.2 and older (verified on 5.3 and 6.0)
React≥ 18

Routing must use conventional routes/ entries. Not supported: Modern.js v2, the legacy pages/ convention, and self-controlled (App.tsx) entries — there's no conventional route tree to type.