Next.js 16.2 typedRoutes Link Href TypeScript Errors Fix

Next.js 16.2 typedRoutes throwing 'Route is not a known route' errors on Link href after upgrade? Here is why the type narrows and how to satisfy the checker.
Next.jsTypeScriptApp Router
May 31, 20266 min read1033 words

The Problem

Upgraded a marketing site from Next.js 15.4 to 16.2 on Tuesday. The build had been green for six months. After the upgrade every dynamic <Link> in the codebase started failing typecheck:

./components/ProductCard.tsx:28:11
Type error: Type '`/products/${string}`' is not assignable to type 'Route'.
The route '`/products/${string}`' is not a known route.

  26 |       <Link
  27 |         href={`/products/${product.slug}`}
> 28 |         className="block hover:underline"
     |           ^
  29 |       >
  30 |         {product.name}
  31 |       </Link>

Same error on dozens of components, all with the same pattern: building an href from a runtime value. The static routes (/about, /contact) compiled fine. Anything with a template literal containing a dynamic segment failed. next build exited with code 1 before the bundler even started.

The codebase had been running with experimental.typedRoutes: true for over a year. Nothing about our Link usage changed in the upgrade. Yet every dynamic href was suddenly considered invalid.

Why It Happens

Next.js 16.2 stabilised typedRoutes and moved it out of the experimental block in next.config.ts. As part of that promotion the type generator stopped accepting template literal patterns that could not be statically narrowed to a known dynamic route.

Under 15.x the generator produced a permissive Route type that included ${string} patterns for any dynamic segment. A href like /products/${slug} matched the pattern /products/${string} and passed. In 16.2 the generator emits a stricter Route type that uses literal route templates from your /app directory. The valid types for a [slug] route now look like:

type Route =
  | '/'
  | '/about'
  | '/contact'
  | `/products/${string}` // still generated
  | '/cart'

The string-template entry is still there, but the inference rules around it changed. A template literal built outside a Route-typed context now resolves to /products/${string} as a generic template literal, which TypeScript no longer considers assignable to the named Route union without an explicit cast or a helper. The compiler sees them as structurally identical but the new branded Route type rejects the implicit conversion.

This is documented in the typedRoutes reference but the upgrade guide does not flag it as a breaking change because the runtime behaviour is identical. Only the typecheck breaks.

The Fix

Three patterns, picked based on how the href is being built.

Pattern 1: Use the Route type from next to brand the string. Cleanest fix for ad-hoc hrefs:

import Link from 'next/link'
import type { Route } from 'next'

interface Props {
  product: { slug: string; name: string }
}

export function ProductCard({ product }: Props) {
  const href = `/products/${product.slug}` as Route

  return (
    <Link href={href} className="block hover:underline">
      {product.name}
    </Link>
  )
}

The cast is a no-op at runtime but tells the compiler that the developer has confirmed the string matches a real route. The branded type accepts it.

Pattern 2: Centralise route builders. When the same dynamic route is built in fifteen places, every cast becomes a copy-paste bug waiting to happen. Move the builder into a typed helper:

// lib/routes.ts
import type { Route } from 'next'

export const routes = {
  product: (slug: string) => `/products/${slug}` as Route,
  category: (slug: string) => `/category/${slug}` as Route,
  blogPost: (slug: string) => `/blog/${slug}` as Route,
} as const

The helper hides the cast behind a function name and makes it impossible to typo the route shape:

import { routes } from '@/lib/routes'

<Link href={routes.product(product.slug)}>{product.name}</Link>

This is what I ended up shipping on the client site, because a regex find-and-replace across the codebase let me migrate fifty-plus components in twenty minutes.

Pattern 3: Switch to the object form. Next.js <Link> accepts href as either a string or an object. The object form takes pathname (a literal route template) and query (params), and the type checker handles dynamic params natively:

<Link
  href={{
    pathname: '/products/[slug]',
    query: { slug: product.slug },
  }}
>
  {product.name}
</Link>

This is the form Next.js recommends in the stabilised API. The downside is more verbose JSX and a refactor across every call site, so I save it for codebases that have not committed to a route-builder helper yet.

One gotcha worth flagging before you ship: the new Route type is regenerated whenever .next/types/routes.d.ts rebuilds, which happens on next build and on the first dev request after a route file changes. If you delete or rename a route while the typecheck is still passing in your IDE, you have stale types. Restart the dev server or run next build to confirm the route graph in the type file matches the filesystem before declaring the migration done.

Verify the migration locally. Run a full typecheck after applying the fix, not just a build, because the build can sometimes mask type errors that only surface in tsc --noEmit:

npx tsc --noEmit
npx next build

Both should exit clean. If the tsc step fails on a file that the build skipped, you have a leftover dynamic href that needs the same treatment.

The Lesson

typedRoutes becoming stable in Next.js 16.2 tightened the type narrowing around dynamic hrefs. Template literals built outside the Route context no longer satisfy the branded type even though they match the pattern. Either cast the result to Route, centralise the build in a typed helper, or move to the object form of href. Whatever you pick, run tsc --noEmit before declaring the upgrade green.

If your Next.js 16 upgrade is wedged on TypeScript errors and the team is running out of patience, that is a job I get paid to land cleanly. See my services. For a related upgrade-time TypeScript pain point, read Next.js 16 searchParams promise TypeError.

Stuck on a Next.js upgrade that broke typecheck? Get it shipped.

Back to blogStart a project