Type-Safe Routing System
Experience end-to-end type safety across your entire application. Dinou combines sub-2ms compile-time route generation with intelligent TypeScript module augmentation, delivering instant IDE autocomplete and dead-link prevention with zero runtime cost.
1. Overview & Zero-Runtime Guarantee
Dinou v7 introduces a built-in, two-level Type-Safe Routing system designed from the ground up to give developers full confidence when building and refactoring applications. Unlike traditional routing setups that require external schema validators or proprietary bundler plugins, Dinou generates strict TypeScript declaration files directly from your src/ filesystem conventions.
1.1. Outward Navigation vs. Inward Consumption
The type system elevates developer experience across two fundamental dimensions:
Instant autocomplete for all valid project paths inside <Link href="..."> and router.push("...").
Catches typos, deleted routes, and broken URLs at compile-time before code ever reaches production.
Strict parameter typing inside page.tsx, layout.tsx, and server-side page_functions.ts via PageProps, LayoutProps, and RouteParams.
Completely eliminates untyped params: any and prevents runtime access to non-existent route parameters.
1.2. Zero-Runtime Cost (0 Extra Bytes)
Everything in Dinou's type-safe routing exists strictly at the TypeScript layer inside auto-generated .d.ts declaration files.
2. Quick Start for Developers
2.2. Level 2: Typed Pages, Layouts & Server Functions
Pass your route pattern as a generic argument to PageProps, LayoutProps, or RouteParams. Dinou infers the exact shape of params without manual interface declarations:
import type { PageProps, LayoutProps, RouteParams } from "dinou";
// 1. src/blog/[slug]/page.tsx
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
// ✅ params.slug is strongly typed as string
return <h1>Post: {params.slug}</h1>;
// ❌ TypeScript error: Property 'id' does not exist on type '{ slug: string }'
// return <h1>{params.id}</h1>;
}
// 2. src/blog/[slug]/layout.tsx
export function BlogLayout({ children, params }: LayoutProps<"/blog/[slug]">) {
// ✅ params.slug is strongly typed; children is typed as ReactNode
return (
<section>
<header>Category: {params.slug}</header>
<main>{children}</main>
</section>
);
}
// 3. src/blog/[slug]/page_functions.ts
export async function getProps(params: RouteParams<"/blog/[slug]">) {
// ✅ Directly types the raw params object without params: any
return {
postTitle: `Post: ${params.slug}`,
};
}2.3. Quick Reference: Core Types
To keep application code clean and expressive, Dinou distills routing type-safety down to primary types exported directly from "dinou":
| Exported Type | Primary Purpose | Canonical Example |
|---|---|---|
| PageProps<T> | React Page components (page.tsx) | Page({ params }: PageProps<"/blog/[slug]">) |
| LayoutProps<T> | React Layout components (layout.tsx) | Layout({ children, params }: LayoutProps<"/blog/[slug]">) |
| ErrorProps<T> | React Error Page components (error.tsx) | Error({ error, params }: ErrorProps<"/blog/[slug]">) |
| RouteParams<T> | Pure functions, data loaders & services (getProps, API helpers) | getProps(params: RouteParams<"/blog/[slug]">) |
| DinouRoute | URL props, <Link>, router.push() & custom navigation components | interface NavProps { href: DinouRoute } |
3. Parameter Mapping Reference Table
Dinou deterministically transforms your file-system folder naming conventions into strict TypeScript definitions:
| Filesystem Convention | Inferred Parameter Type | Route Example | Type of params |
|---|---|---|---|
| Static Route | No parameters | src/about/page.tsx | Record<string, never> |
Single Dynamic [id] | string | src/blog/[id]/page.tsx | { id: string } |
Catch-All [...slug] | string[] | src/docs/[...slug]/page.tsx | { slug: string[] } |
Optional Dynamic [[lang]] | string | undefined | src/[[lang]]/page.tsx | { lang?: string } |
Optional Catch-All [[...opt]] | string[] | undefined | src/shop/[[...opt]]/page.tsx | { opt?: string[] } |
| Multiple Dynamic Segments | Combined by segment | src/users/[uId]/posts/[pId] | { uId: string; pId: string } |
| Nested Optionals | Positional dependent | src/inv/[[wh]]/[[aisle]] | { wh?: string; aisle?: string } |
For routes containing optional segments ([[slug]] or [[...opt]]), Dinou automatically registers both the parameterized path and the base route where the segment is omitted.
For example, with src/products/[[category]]/page.tsx, both <Link href="/products" /> and <Link href="/products/electronics" /> are valid, autocompleted routes.
Be cautious with trailing slashes in dynamic links!
In TypeScript, template literal types evaluate ${string} as matching any string, including the empty string "" (because "" extends string is true).
The scenario: Consider a nested route such as src/posts/[id]/[slug]/page.tsx, whose generated template literal type is `/posts/${string | number}/${string | number}`.
If a developer accidentally leaves off the second parameter but appends a trailing slash:
TypeScript matches the first parameter with id, the slash with literal /, and the second parameter with empty string "". However, at runtime Dinou normalizes trailing slashes, requesting /posts/123 (which results in a 404 Not Found because no page.tsx exists at the parent segment).
Best Practice: Always avoid trailing slashes when building dynamic URLs. Written without the trailing slash (`/posts/${id}`), TypeScript accurately flags the missing parameter at compile-time.
4. Advanced Usage & Real-World Recipes
4.1. IDE Autocomplete Workflow (Ctrl + Space)
When typing a component, type PageProps<""> and press Ctrl + Space (or Cmd + Space). Your editor will list every registered route pattern in the project:
import type { PageProps } from "dinou";
// Trigger Ctrl + Space inside "" to pick from project routes
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
return <h1>{params.slug}</h1>;
}4.2. Typed Error Pages with ErrorProps
In Dinou, error pages (error.tsx and parallel slot errors @slot/error.tsx) receive two props: error: Error and params. You can strictly type them directly using ErrorProps<T>:
"use client";
import { useRouter, type ErrorProps } from "dinou";
export default function BlogPostError({ params, error }: ErrorProps<"/blog/[slug]">) {
const router = useRouter();
return (
<div className="error-card p-6 border rounded-xl bg-destructive/10">
<h2>Failed to load post: {params.slug}</h2>
<p className="text-sm text-muted-foreground">{error.message}</p>
{/* Soft reload via Dinou client router */}
<button onClick={() => router.refresh()}>Try Again</button>
</div>
);
}4.3. Custom Navigation Components with DinouRoute
When building reusable UI components (custom buttons, navigation tabs, or breadcrumb menus), type their destination props with DinouRoute to give your design system native compile-time link validation:
import type { DinouRoute } from "dinou";
import { Link } from "dinou";
interface NavButtonProps {
href: DinouRoute; // 👈 Inherits full autocompletion and route validation
label: string;
}
export function NavButton({ href, label }: NavButtonProps) {
return (
<Link href={href} className="px-3 py-1.5 rounded-lg bg-blue-600 text-white font-medium">
{label}
</Link>
);
}
// In your application code:
const menu: { label: string; href: DinouRoute }[] = [
{ label: "Overview", href: "/docs" }, // ✅ Autocompleted
{ label: "Routing", href: "/docs/routing" }, // ✅ Autocompleted
// { label: "Broken", href: "/non-existent-page" }, // ❌ TypeScript compile error!
];4.4. Dynamic Navigation with Typed Interpolation
Template string URLs are verified against parameterized literal types:
import { Link, useRouter } from "dinou";
interface Product {
id: string;
name: string;
}
export function ProductCard({ product }: { product: Product }) {
const router = useRouter();
return (
<div className="card p-4 border rounded-xl">
{/* String interpolation verified against `/products/${string | number}` */}
<Link href={`/products/${product.id}`}>{product.name}</Link>
<button onClick={() => router.push(`/products/${product.id}`)}>
View Details
</button>
</div>
);
}5. Technical Architecture: Under the Hood
Dinou coordinates compilation, route scanning, and TypeScript declarations through a zero-dependency architecture:
src/ Directory
Synchronous read of all page & layout files.
generate-route-types
In-memory scanner completes in < 2ms.
routes.d.ts
Augments "dinou" module contract.
dinou-env.d.ts
Zero-config TypeScript autocompletion.
Implemented in dinou/core/generate-route-types.js. Pure filesystem parsing without executing user code, importing components, or launching test servers.
- Ignores private folders (
_*) and parallel slots (@*). - Transparently unwraps route groups (
(group)).
Generates .dinou/types/routes.d.ts augmenting the core "dinou" module with:
DinouStaticRoutes: Union of exact string routes.DinouDynamicRoutes: Parameterized template literal types.DinouRouteParamsMap: Mapping from path pattern to param dictionary.
Types like DinouRouter.Register, DinouRoutePattern, and ExtractRouteParams<T> are exported to satisfy TypeScript declaration emit rules.
They allow module augmentation to wire seamlessly into the global type contract without polluting your daily application code.
At boot, Dinou ensures the root dinou-env.d.ts contains:
Ensures IDEs and CI discover route typings immediately without custom tsconfig.json path aliases.
6. Lifecycle: Dev Server & Production Builds
How Dinou keeps your route typings strictly synchronized across local development and deployment pipelines:
Runs immediately at boot, then arms a low-overhead filesystem watcher on src/:
- Detects added, renamed, or deleted
page.(tsx|jsx|ts|js)files. - Regenerates
.dinou/types/routes.d.tsin under 2ms. - IDE autocomplete reflects changes instantly with no server restarts.
All bundlers (esbuild, Rollup, Webpack, Bun, Deno, Cloudflare) run the scanner before bundling:
- Guarantees route typings exist before
tsc --noEmitexecutes in CI/CD. - Deterministic builds in clean, gitignored container environments (GitHub Actions, Docker, Vercel).
7. Backward Compatibility Guarantee
Existing components without explicit generic parameters continue to work without modification.
You can continue using export default function Page({ params }: any) or generic PageProps during progressive migrations.
If .dinou/types/routes.d.ts has not been generated yet or if a project operates in untyped JavaScript, DinouRoute gracefully falls back to standard string.
Zero breaking changes to existing setups or non-TypeScript workflows.