FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Route export validation rejects string literal union params in layouts and pages (still present in 16.3.1) · Issue #97523 · vercel/next.js · GitHub

/ next.js Public

Route export validation rejects string literal union params in layouts and pages (still present in 16.3.1) #97523

Description

Link to the code that reproduces this issue

https://github.com/darthmaim-reproductions/vercel-next.js-82820

To Reproduce

  1. Clone the linked reproduction and install dependencies.

  2. Note the app structure — a root dynamic segment with an exhaustive param set:

    app/[locale]/layout.tsx

    export const dynamicParams = false;
    
    type Locale = "en" | "de";
    
    export async function generateStaticParams(): Promise<{ locale: Locale }[]> {
      return [{ locale: "en" }, { locale: "de" }];
    }
    
    export default async function LocaleLayout({
      children,
      params,
    }: {
      children: React.ReactNode;
      params: Promise<{ locale: Locale }>;
    }) {
      const { locale } = await params;
      return <html lang={locale}><body>{children}</body></html>;
    }

Run next build.

Compilation succeeds. Type checking then fails:

.next/types/validator.ts
Type 'typeof import("app/[locale]/layout")' does not satisfy the constraint
'LayoutConfig<"/[locale]">'.
Types of property 'default' are incompatible.
Types of property 'params' are incompatible.
Type 'Promise<{ locale: string; }>' is not assignable to
type 'Promise<{ locale: "en" | "de"; }>'.
Open .next/types/routes.d.ts and confirm the generated map widens the segment
regardless of what generateStaticParams returns:

interface ParamMap {
"/[locale]": { "locale": string; }
}
Replace the param type with Promise<{ locale: string }> (or LayoutProps<"/[locale]">)
and rebuild — the error disappears, confirming the union is the only cause.

Optional, to confirm the runtime is unaffected: run next build with
typescript: { ignoreBuildErrors: true }. The build completes and every route
renders correctly with the narrow type in place.

Current vs. Expected behavior

Current

ParamMap types every dynamic segment as string, derived from the folder name alone.
The return type of generateStaticParams is not taken into account.

As a result, declaring a param with a narrower type — a string literal union or an enum —
fails route export validation, because the generated contract promises the component may
be called with any string:

Type 'Promise<{ locale: string; }>' is not assignable to
type 'Promise<{ locale: "en" | "de"; }>'.

The build fails at type checking. The application itself is correct: it compiles, every
route renders, and no invalid locale can reach it.

This affects layouts and pages alike. generateStaticParams is affected too, with its own
signature ({ params: ParamMap[Route] }, no Promise).

Expected

When generateStaticParams declares a typed return and dynamicParams = false, the set
of possible values is fully known at build time — in the same file. That type should flow
into ParamMap, and therefore into PageProps and LayoutProps:

interface ParamMap {
  "/[locale]": { "locale": "en" | "de" }
}
Failing that, any supported way to declare the param type of a route would resolve it 
an opt-in generic, a module augmentation point, or a config entry.

Why this matters
dynamicParams = false already guarantees at runtime what the type expresses: unknown
segments return 404. The validator rejects the app's type for being more precise than
the contract, and forces one of two regressions:

widen to string, then cast or guard in the body  the same mismatch, moved into every file, unverified at the boundary;
typescript: { ignoreBuildErrors: true }  which disables the whole validator, including the parts that catch real mistakes.
Both discard correct typing to satisfy an imprecise contract.

### Provide environment information

```bash
Operating System:
  Platform: win32
  Arch: x64
  Version: Windows 10 Home
Binaries:
  Node: 24.13.1
  npm: 11.3.0
Relevant Packages:
  next: 16.3.1 // Latest available version is detected (16.3.1).
  react: 19.2.8
  react-dom: 19.2.8
  typescript: 5.9.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

TypeScript, Dynamic Routes

Which stage(s) are affected? (Select all that apply)

next build (local)

Additional context

Opening a new issue rather than commenting, since both channels above are dead ends.

The behaviour has persisted across three minor releases

Reported on 15.5, still reproducible on 16.3.1. Verified on a clean build after deleting
.next, so it is not a stale-cache artefact.

16.3 shipped next/root-params, which gives typed access to root segments — its generated
getter returns Promise<string>, so it widens the same way and does not help here.

Note on the documentation

The LayoutProps / PageProps reference states these helpers provide "strongly typed
params". That is accurate for param names, which are inferred from the directory
structure, and for the route string itself. It is not accurate for param values, which
are always string.

The wording sets an expectation the helper does not meet, and it is likely why this keeps
being reported as a bug rather than a feature request. Worth clarifying in the docs
regardless of what happens to this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Dynamic RoutesRelated to dynamic routes.TypeScriptRelated to types with Next.js.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


    Back | FazBrowse Home | New Git URL