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.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.
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.tsx
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:
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
cannot be commented on or reopened. Its reproduction is the one linked above.
a route param alongside the generated LayoutProps. A Vercel member pointed to a PR
there, which the author reported does not address LayoutConfig. 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.