11.1 App Router Type System — Page, Layout, and params Types
File-Based Routing in Next.js App Router
Next.js 13+ App Router uses file system-based routing. Understanding the types of each file is essential when using TypeScript.
app/
├── layout.tsx # Root layout
├── page.tsx # Home page (/)
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
└── dashboard/
├── layout.tsx # Dashboard layout
└── page.tsx # /dashboard
Page Component Types
Static Pages
// app/page.tsx
export default function HomePage() {
return <h1>Home</h1>;
}
params and searchParams
// app/blog/[slug]/page.tsx
// Next.js 15+ (async params)
interface PageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function BlogPost({ params, searchParams }: PageProps) {
const { slug } = await params;
const { page, sort } = await searchParams;
const post = await getPostBySlug(slug);
return (
<article>
<h1>{post.title}</h1>
<p>Page: {page ?? '1'}</p>
</article>
);
}
// Next.js 14 and below (sync params)
interface PageProps {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default function BlogPost({ params, searchParams }: PageProps) {
const { slug } = params;
// ...
}
Nested Dynamic Routes
// app/shop/[category]/[productId]/page.tsx
interface PageProps {
params: Promise<{
category: string;
productId: string;
}>;
}
export default async function ProductPage({ params }: PageProps) {
const { category, productId } = await params;
const product = await getProduct(category, productId);
return <ProductDetail product={product} />;
}
Catch-all Routes
// app/docs/[...slug]/page.tsx
interface PageProps {
params: Promise<{ slug: string[] }>;
}
// When accessing app/docs/[...slug]: slug = ['getting-started', 'installation']
// Optional catch-all: app/docs/[[...slug]]/page.tsx
interface OptionalPageProps {
params: Promise<{ slug?: string[] }>;
}
generateStaticParams
Define paths to generate statically at build time.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
// Return type: { slug: string }[]
return posts.map(post => ({
slug: post.slug,
}));
}
// Nested dynamic routes
// app/[category]/[id]/page.tsx
export async function generateStaticParams() {
const categories = await getCategories();
const params: { category: string; id: string }[] = [];
for (const category of categories) {
const items = await getItemsByCategory(category.slug);
items.forEach(item => {
params.push({ category: category.slug, id: item.id });
});
}
return params;
}
Layout Component Types
// app/layout.tsx (Root layout)
import { ReactNode } from 'react';
interface RootLayoutProps {
children: ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Nested Layouts
// app/dashboard/layout.tsx
interface DashboardLayoutProps {
children: ReactNode;
}
export default function DashboardLayout({ children }: DashboardLayoutProps) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}
Parallel Route Layout
// app/layout.tsx — includes @modal slot
interface RootLayoutProps {
children: ReactNode;
modal: ReactNode; // @modal slot
}
export default function RootLayout({ children, modal }: RootLayoutProps) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
);
}
Special File Types
loading.tsx
// app/blog/loading.tsx
export default function Loading() {
return <div>Loading...</div>;
}
error.tsx
// app/blog/error.tsx
'use client';
interface ErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function Error({ error, reset }: ErrorProps) {
return (
<div>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
not-found.tsx
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - Page Not Found</h2>
</div>
);
}
Pro Tips
1. Route type safety — next-safe-navigation
import { createNavigationConfig } from 'next-safe-navigation';
import { z } from 'zod';
const { routes, useSafeParams, useSafeSearchParams } =
createNavigationConfig((defineRoute) => ({
home: defineRoute('/'),
blog: defineRoute('/blog'),
blogPost: defineRoute('/blog/[slug]', {
params: z.object({ slug: z.string() }),
}),
search: defineRoute('/search', {
search: z.object({
q: z.string().optional(),
page: z.coerce.number().default(1),
}),
}),
}));
// Type-safe links
<Link href={routes.blogPost({ params: { slug: 'hello-world' } })} />
// Type-safe params access
const { slug } = useSafeParams('blogPost');
2. generateMetadata types
import { Metadata, ResolvingMetadata } from 'next';
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata(
{ params }: PageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.coverImage],
},
};
}