# Next.js Typed Routes in 2026: Enabling `typedRoutes` Experimentally and What It Catches That TypeScript Alone Misses

> Source: <https://dev.to/jsmanifest/nextjs-typed-routes-in-2026-enabling-typedroutes-experimentally-and-what-it-catches-that-2npb>
> Published: 2026-10-07 06:06:57+00:00

`typedRoutes` Experimentally and What It Catches That TypeScript Alone Misses
*This article was written with the assistance of AI, under human supervision and review.*

Most route bugs in Next.js stem from string literals that TypeScript treats as valid code but that fail at runtime. A developer writes `/products` when the folder structure defines `/product`, or passes `id: "123"` to a route expecting a numeric segment. TypeScript compiles both without complaint. The user clicks a link and lands on a 404 page. The cost appears later, in support tickets or lost conversions, not in the editor where the mistake was made.

The `typedRoutes` experimental flag changes this. When enabled, Next.js generates type definitions from the actual file structure in `app/` and makes every route reference a type-checked operation. A route that does not exist becomes a compile-time error. A dynamic segment that receives the wrong type triggers a red squiggle before the code reaches production.

This matters because the feedback loop compresses from hours or days to seconds. Developers see the mistake where they made it. The distinction between runtime discovery and compile-time prevention is the difference between shipping broken links and catching them before commit.

`app/` directory structure, turning route strings into type-checked values.`href` props all gain type safety when the flag is enabled.
Enabling `typedRoutes` requires three conditions: a Next.js project using App Router, TypeScript 5.0 or later, and the experimental flag set in the config file.

The configuration itself is a single property in `next.config.ts`:

``` python
import type { NextConfig } from 'next'

const config: NextConfig = {
  experimental: {
    typedRoutes: true,
  },
}

export default config
```

When the dev server starts with this flag, Next.js scans the `app/` directory and writes generated type definitions to `.next/types/link.d.ts`. This file contains a union type of every valid route path in the application. The TypeScript language server reads this file and enforces the types in Link components, `useRouter` hooks, and redirect calls.

The `.next/types` directory is build-time output. It should be added to `.gitignore` alongside `.next/`. When another developer clones the repository and runs `npm run dev`, Next.js regenerates the types for their local file structure. This regeneration happens automatically on every file system change that affects routes (adding a folder, deleting a page, renaming a dynamic segment).

The TypeScript version matters because the generated types rely on template literal types and recursive conditional types introduced in TypeScript 4.1 and refined in 5.0. A project stuck on TypeScript 4.9 will see type errors in the generated definitions themselves. Upgrading TypeScript is a prerequisite, not an optional step.

The `.next/types/link.d.ts` file is a single type definition that exports a `Route` type. This type is a union of string literals representing every route the application defines.

For a simple app structure:

```
app/
  page.tsx
  about/
    page.tsx
  blog/
    [slug]/
      page.tsx
```

Next.js generates a Route type approximately like this:

```
export type Route = "/" | "/about" | `/blog/${string}`
```

Dynamic segments appear as template literal types. The `[slug]` folder becomes `/blog/${string}`. Optional catch-all segments `[[...slug]]` generate more complex unions. The generated file is read-only. Modifying it manually does nothing because the next build overwrites the changes.

The Link component from `next/link` uses this Route type to constrain its `href` prop. When developers write `<Link href="https://dev.to/products">`, TypeScript checks if `"/products"` exists in the Route union. If the route is `/product` (singular), the editor shows an error before the code runs.

The same type flows into the `useRouter` hook from `next/navigation`. Calling `router.push("/invalid")` fails to compile when `/invalid` is not part of the Route union. The `redirect` function from `next/navigation` also accepts the Route type, catching invalid server-side redirects at compile time instead of runtime.

This generation step is deterministic. Given the same file structure, Next.js always produces the same type definitions. The types update instantly when route files change. Adding a new page to `app/dashboard/settings/page.tsx` immediately adds `"/dashboard/settings"` to the Route union without restarting the dev server.

TypeScript validates that code is syntactically correct and type-consistent but cannot verify that a string matches a file path. The `typedRoutes` flag solves this by making the file system the source of truth for route types.

Without `typedRoutes`, this code compiles:

``` python
import Link from 'next/link'

export default function Navigation() {
  return (
    <div>
      <Link href="https://dev.to/products">Products</Link>
      <Link href="https://dev.to/about-us">About</Link>
    </div>
  )
}
```

If the actual routes are `/product` and `/about`, both links break at runtime. TypeScript sees two string literals assigned to a prop expecting a string. That is valid. The compiler has no concept of route existence.

With `typedRoutes`, the same code triggers a compile-time error. The Route union contains `"/product"` but not `"/products"`. TypeScript flags the mismatch. The developer fixes the typo before committing the code.

Dynamic segments add another layer of safety. A route defined as `app/blog/[slug]/page.tsx` expects a slug parameter. Without `typedRoutes`, this code compiles but fails at runtime:

```
<Link href="https://dev.to/blog">Blog</Link>
```

The route requires a slug. Navigating to `/blog` without one results in a 404. With `typedRoutes`, TypeScript enforces that the href matches the template `/blog/${string}`. The developer must provide a slug:

```
<Link href="https://dev.to/blog/introduction">Blog</Link>
```

Query parameters remain untyped even with the flag enabled. Writing `href="https://dev.to/products?sort=price"` compiles whether or not the products page uses a `sort` query param. The Route type only captures path segments, not query strings. This limitation is fundamental. Query parameters are runtime values that can be appended to any route, so the type system cannot enumerate them statically.

The implication here is that `typedRoutes` prevents path errors but does not replace runtime validation for query parameters. Developers still need to validate searchParams in server components or query strings in client components using libraries like Zod or manual checks.

The practical benefit of `typedRoutes` appears most clearly in navigation code. The Link component, `useRouter` hook, and `redirect` function all consume the Route type and enforce it at different layers of the application.

A navigation menu with `typedRoutes` enabled looks like this:

``` python
import Link from 'next/link'

export default function MainNav() {
  return (
    <nav>
      <Link href="https://dev.to/">Home</Link>
      <Link href="https://dev.to/about">About</Link>
      <Link href="https://dev.to/blog/nextjs-routing">Latest Post</Link>
      {/* This line triggers a type error if /products does not exist */}
      <Link href="https://dev.to/products">Products</Link>
    </nav>
  )
}
```

If the `/products` route does not exist in the file structure, TypeScript underlines the string with a red squiggle. The error message states that `"/products"` is not assignable to type `Route`. The developer sees the mistake immediately and corrects it to the actual route (perhaps `/product` or `/shop`).

Client-side navigation using `useRouter` gains the same safety:

``` js
'use client'

import { useRouter } from 'next/navigation'

export default function ProductCard({ id }: { id: string }) {
  const router = useRouter()

  const handleClick = () => {
    // Type-safe: Next.js knows /product/[id] exists
    router.push(`/product/${id}`)

    // Type error: /products/[id] does not exist in file structure
    // router.push(`/products/${id}`)
  }

  return (
    <button onClick={handleClick}>
      View Product
    </button>
  )
}
```

The template literal `/product/${id}` matches the Route type because `app/product/[id]/page.tsx` exists. The commented-out line would fail to compile if uncommented, preventing the runtime 404 before the code reaches production.

Server actions and route handlers also benefit. The `redirect` function from `next/navigation` accepts the Route type:

``` js
import { redirect } from 'next/navigation'

export async function createProduct(formData: FormData) {
  const productId = await saveProduct(formData)

  // Type-safe redirect to dynamic route
  redirect(`/product/${productId}`)

  // Type error: this route does not exist
  // redirect('/products/success')
}
```

This enforcement extends to nested layouts and parallel routes. If the app defines `@modal/(.)product/[id]/page.tsx` as a parallel route, the Route type includes that path. Referencing it in a Link or redirect call becomes type-safe.

The code examples above show the primary use case: preventing 404s caused by typos or refactoring oversights. When a route moves from `/user/[id]` to `/profile/[id]`, every reference to `/user/${id}` becomes a type error. The developer cannot merge the PR until every reference updates. This catches breaking changes that manual code review might miss.

The `typedRoutes` flag enforces type safety for route paths but does not cover several navigation scenarios. Understanding where it fails prevents false confidence in type coverage.

Middleware rewrites and redirects bypass the type system. When middleware rewrites `/old-path` to `/new-path`, the Route type knows nothing about the rewrite. The application allows navigation to `/old-path` at runtime even though no `app/old-path/page.tsx` exists. TypeScript flags `/old-path` as invalid, but the code works in production. This mismatch forces developers to disable the type check or maintain a separate list of middleware-handled paths.

External links pose the same problem. Writing `<Link href="https://example.com">` triggers a type error because `https://example.com` is not part of the Route union. The recommended workaround is casting the href to `any` or using an anchor tag instead of Link for external URLs. Neither solution is satisfying. The type system cannot distinguish between same-origin and cross-origin URLs, so it rejects both invalid internal routes and valid external links with the same error.

Dynamic imports and lazy-loaded routes also escape type checking. When a route is code-split and loaded on demand, the Route type includes it, but the runtime behavior depends on chunk availability. A network failure that prevents loading the route chunk results in a runtime error that `typedRoutes` does not predict.

Query parameter validation remains outside the scope of the feature. As mentioned earlier, the Route type captures path segments but ignores query strings. A route expecting `?page=1` accepts `?page=invalid` without complaint. Developers must layer in runtime validation using libraries or manual checks.

Optional catch-all segments `[[...slug]]` generate overly permissive types. The route `app/docs/[[...slug]]/page.tsx` produces a Route type like `/docs` | `/docs/${string}`. This accepts `/docs/a/b/c/d/e` even if the application logic only handles two-level paths. The type system has no way to encode "up to N segments" without listing every valid combination explicitly.

Hash fragments (`#section`) and state objects passed to `router.push` are also untyped. The Route type only validates the pathname portion of the URL. Passing `router.push("/blog/post", { state: { scroll: false } })` compiles even if the state object is malformed.

The failure mode here is subtle but expensive. Developers enable `typedRoutes` expecting full route safety and later discover that middleware, external links, and query parameters still require manual validation. The type system provides coverage for static paths and simple dynamic segments, not comprehensive navigation safety.

Adding `typedRoutes` to an established codebase requires planning. The types generated by the flag will immediately flag hundreds or thousands of route references, depending on project size. A gradual rollout avoids paralysis.

The first step is enabling the flag in `next.config.ts` and running the dev server to generate the initial types. This creates `.next/types/link.d.ts` and surfaces every route reference that does not match the file structure. The error count might be overwhelming. Do not attempt to fix all errors at once.

The second step is triaging errors by route importance. Start with the most critical user flows: authentication, checkout, dashboard navigation. Fix those routes first. The rest can wait. This ensures that the highest-value parts of the application gain type safety immediately while less critical pages remain in the backlog.

Type assertions provide a temporary escape hatch. When a route reference is valid but the type system rejects it (such as middleware rewrites), casting to `any` or using a type guard allows the code to compile:

```
<Link href={"/legacy-path" as any}>Old Link</Link>
```

This approach is pragmatic but accumulates technical debt. Each assertion is a spot where the type system cannot help. Minimize them. Track them in comments or a task list for eventual removal.

Refactoring route structures during migration is tempting but dangerous. If the file structure changes (renaming folders, consolidating routes), the Route type updates immediately. Every existing route reference breaks. Changing both the file structure and fixing route references in the same PR creates a large, risky changeset. Separate the two. Fix existing references first, then refactor the file structure in a follow-up PR.

Testing remains essential. The `typedRoutes` flag catches path errors at compile time but does not validate application logic. A route might exist and type-check correctly but still render the wrong content or fail to handle edge cases. Integration tests that navigate between routes ensure the application works end-to-end, not just that the paths are spelled correctly.

The migration timeline depends on codebase size and team capacity. A small application with 20 routes can migrate in a day. A large enterprise app with hundreds of routes might take weeks. The strategy is the same: enable the flag, prioritize critical flows, fix incrementally, ship often.

No. The feature requires App Router. Pages Router uses file-based routing but does not generate route types. Projects using Pages Router must migrate to App Router to gain typed route support.

Yes, but the custom server must delegate routing to Next.js. If the custom server handles routes independently (such as Express routes that bypass Next.js), those routes remain untyped. Only routes defined in `app/` gain type safety.

Next.js stops generating `.next/types/link.d.ts`. Existing route references lose type checking and revert to accepting any string. The code continues to work at runtime, but type safety disappears.

Minimally. Type generation adds a few hundred milliseconds to the initial dev server startup. Incremental updates when routes change are near-instant. Production builds see no measurable impact because the type generation happens once during the build step.

No. The Route type is generated automatically from the file structure and cannot be extended manually. Projects needing custom route validation must layer additional runtime checks on top of the generated types.

The `typedRoutes` flag prevents a specific class of bugs (invalid route references) at the cost of additional build complexity and an experimental status. Whether to enable it depends on how frequently route errors occur in your workflow and whether your team has the TypeScript expertise to manage the generated types.

Teams with large codebases, frequent route refactoring, or a history of 404 bugs will see immediate value. The type system catches errors before they reach production. The feedback loop compresses to seconds. The cost is negligible for projects already using TypeScript.

Smaller projects with stable route structures might not benefit. If routes rarely change and manual testing catches errors reliably, the added complexity of managing generated types might outweigh the safety gains.

The experimental status matters. Next.js might change the implementation, deprecate the feature, or promote it to stable with breaking changes. Projects enabling `typedRoutes` should monitor Next.js release notes and be prepared to adapt if the API shifts.

That covers the essential patterns for typed routes in Next.js. Enable the flag in development first, validate that the generated types match your expectations, and gradually roll it out to production once your team understands the tradeoffs. The difference between catching route errors at compile time versus runtime is immediate.
