TypeScript and Next.js 14: A Perfect Match
Next.js 14 ships with TypeScript support out of the box. Every App Router API — from Server Components to generateMetadata to route handlers — is fully typed. This guide covers the type patterns you'll use daily in a Next.js 14 TypeScript project.
Server Components: Default and Async
App Router components are Server Components by default. They can be async — and they should be when they fetch data:
// app/users/page.tsx
import { db } from '@/lib/db';
interface User {
id: number;
name: string;
email: string;
}
export default async function UsersPage() {
const users: User[] = await db.user.findMany();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Client Components
Add "use client" when you need hooks or browser APIs. TypeScript types work exactly as in React:
"use client";
import { useState } from 'react';
interface Props {
initialCount: number;
}
export default function Counter({ initialCount }: Props) {
const [count, setCount] = useState<number>(initialCount);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Dynamic Routes and Params
In Next.js 14, route params are Promises. The correct type for page.tsx props is:
// app/blog/[slug]/page.tsx
type Params = Promise<{ slug: string }>;
export default async function BlogPost({ params }: { params: Params }) {
const { slug } = await params;
// ...
}generateMetadata Typing
Use Next.js's built-in Metadata and ResolvingMetadata types:
import { Metadata, ResolvingMetadata } from 'next';
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
images: [post.coverImage],
},
};
}Route Handlers (API Routes)
Route handlers in the App Router use Web API Request/Response:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q') ?? '';
const users = await searchUsers(query);
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body: unknown = await request.json();
const parsed = UserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.flatten() },
{ status: 400 }
);
}
const user = await createUser(parsed.data);
return NextResponse.json(user, { status: 201 });
}Server Actions
Server Actions are async functions that run on the server. Type them with proper return types:
"use server";
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const CreatePostSchema = z.object({
title: z.string().min(1),
content: z.string().min(10),
});
export async function createPost(
formData: FormData
): Promise<{ success: true } | { success: false; error: string }> {
const result = CreatePostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!result.success) {
return { success: false, error: result.error.message };
}
await db.post.create({ data: result.data });
revalidatePath('/posts');
return { success: true };
}For more TypeScript patterns, use our JS to TypeScript converter to automatically type-annotate your JavaScript files.
