Skip to main content
Advertisement

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],
},
};
}
Advertisement