Route export validation rejects string literal union params in layouts and pages (still present in 16.3.1)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.5k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://github.com/darthmaim-reproductions/vercel-next.js-82820
To Reproduce
-
Clone the linked reproduction and install dependencies.
-
Note the app structure — a root dynamic segment with an exhaustive param set:
app/[locale]/layout.tsxexport 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
- #82820 describes this exact behaviour on 15.5. It is closed and locked, so it
cannot be commented on or reopened. Its reproduction is the one linked above. - Discussion #83450 asks the same question — how to declare a string literal union for
a route param alongside the generatedLayoutProps. A Vercel member pointed to a PR
there, which the author reported does not addressLayoutConfig. No resolution.
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the linked reproduction and run next build, then inspect .next/types/validator.ts and .next/types/routes.d.ts alongside app/[locale]/layout.tsx. Trace how generateStaticParams, dynamicParams = false, LayoutProps, and PageProps are represented in the generated contracts. Done means narrow literal-union params pass validation for both pages and layouts without ignoring build errors.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- developer-experience, web-dev
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100