Special Sponsor:PromptBuilder— Fast, consistent prompt creation powered by 1,000+ expert templates.
Make your Product visible here.Contact Us
Taking new clients

Need a dev team that ships?

011BQ builds TypeScript-first products, migrations, and internal tools for startups and scale-ups.

  • JS/TS migration & codebase modernisation
  • Custom dev tools & internal platforms
  • React, Next.js & Node.js engineering
  • Code review, architecture & tech advisory

Or reach us directly

011bq.com

Send us a message

We respond within 1 business day.

check_dark

Thank You!

Your message has been successfully sent. We will get back to you soon!

Message sent!

Thanks for reaching out. The 011BQ team will get back to you within 1 business day.

HomeChevronBlogChevronHow to Use TypeScript with Next.js 14 App Router: Complete Guide

How to Use TypeScript with Next.js 14 App Router: Complete Guide

j
js2ts Team
22/05/2026·2 minutes 4 seconds read
How to Use TypeScript with Next.js 14 App Router: Complete GuideHow to Use TypeScript with Next.js 14 App Router: Complete Guide

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.

Share