AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Route Export Validation Rejects String Literal Union Params In Layouts And Pages

Next.js generated ParamMap widens dynamic segment values to string, ignoring the return type of generateStaticParams even when dynamicParams is false. This causes components (layouts/pages) that declare narrower types such as 'en' | 'de' to fail type validation during build, despite correct runtime behavior.

mediumConfidence 95%Next.jsAffected V15.5.0Affected V16.3.1

Origin Analysis

The Next.js route type generator derives dynamic segment parameter types solely from filesystem segment names (e.g., [locale] -> { locale: string }). It does not introspect the return type of generateStaticParams or account for dynamicParams = false. Therefore the generated LayoutConfig and PageProps contracts always expect string, and any user-declared narrower type is incompatible.
1. Create app/[locale]/layout.tsx with dynamicParams = false and a generateStaticParams returning a typed union e.g. Promise<{ locale: 'en' | 'de' }[]>. 2. Give the layout component a params prop of Promise<{ locale: 'en' | 'de' }>. 3. Run next build. 4. Compilation succeeds, but type checking fails with: Type 'Promise<{ locale: string; }>' is not assignable to type 'Promise<{ locale: "en" | "de"; }>'. 5. Replace the component params type with LayoutProps<"/[locale]"> or Promise<{ locale: string }> to confirm the error disappears.

Fixing Code Block

import type { LayoutProps } from 'next'; type Locale = 'en' | 'de'; export const dynamicParams = false; export async function generateStaticParams(): Promise<{ locale: Locale }[]> { return [{ locale: 'en' }, { locale: 'de' }]; } function isLocale(value: string): value is Locale { return value === 'en' || value === 'de'; } export default async function LocaleLayout({ children, params, }: LayoutProps<'/[locale]'>) { const { locale } = await params; if (!isLocale(locale)) { throw new Error(`Unexpected locale: ${locale}`); } return <html lang={locale}><body>{children}</body></html>; }
This hotfix uses the generated LayoutProps<'/[locale]'> type, which satisfies the route validator because it expects params: Promise<{ locale: string }>. Inside the component, the awaited locale is narrowed via a custom type guard isLocale, ensuring that only the valid union members are used. This avoids a cast and keeps a single source of truth for allowed values. It unblocks the build while preserving runtime safety.

Edge Case Audit

The fix does not address the root cause; the boundary type is still widened to string, so invalid values could theoretically be passed if generateStaticParams logic changes without updating the type guard. The guard duplicates the valid values, which can drift from generateStaticParams. If dynamicParams is ever set to true, unexpected strings will now throw at runtime instead of being handled by Next's 404. To roll back, remove the type guard and either widen the component prop to Promise<{ locale: string }> or revert to the original narrower type and set typescript.ignoreBuildErrors to true until upstream fixes the generator.

Ecosystem Topology