【TypeScript × Next.js】App Router で型安全な開発を実現する完全ガイド|Server Components・Route Handlers・Server Actions・Metadata の型定義を徹底解説

【TypeScript × Next.js】App Router で型安全な開発を実現する完全ガイド|Server Components・Route Handlers・Server Actions・Metadata の型定義を徹底解説 TypeScript

Next.js の公式スターターは TypeScript をデフォルトで採用しており、現在のフロントエンド開発では TypeScript × Next.js が事実上の標準構成です。しかし Next.js 13 以降の App Router では、従来の Pages Router とは大きく異なる型定義が必要になります。

この記事では App Router における page.tsx・layout.tsx・Route Handlers・Server Actions・Metadata など、App Router 固有のファイルごとの型定義を実例つきで徹底解説します。

この記事でわかること

  • App Router の各ファイル(page / layout / error / loading)の正確な Props 型
  • 動的ルート(Dynamic Routes)の params / searchParams
  • Server Components と Client Components の型の違い
  • Route Handlers(API Routes)の NextRequest / NextResponse
  • Server Actions の型安全なフォーム処理
  • Metadata API・generateStaticParams の型定義
  • 環境変数を Zod で型安全に管理する方法
  • App Router で発生しやすい型エラーと解消法
対象バージョン
Next.js 14 以降(App Router)/ TypeScript 5.x。Pages Router(pages/ ディレクトリ)の型定義は含みません。React + TypeScript の基本についてはTypeScript React ガイドを参照してください。
スポンサーリンク

プロジェクトセットアップ

新規プロジェクトの作成

# TypeScript を含む Next.js プロジェクトを作成
npx create-next-app@latest my-app --typescript --tailwind --eslint --app

# 既存プロジェクトに TypeScript を追加する場合
npm install --save-dev typescript @types/react @types/react-dom @types/node
# → 次回 next dev 実行時に tsconfig.json が自動生成される

Next.js が生成する tsconfig.json

// tsconfig.json(Next.js デフォルト + 解説コメント付き)
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,          // ← 重要:strict モードが有効
    "noEmit": true,          // ← Next.js がビルドするので tsc は型チェックのみ
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",  // ← Next.js 13.2+ の推奨設定
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],   // ← Next.js 専用プラグイン
    "paths": {
      "@/*": ["./src/*"]              // ← パスエイリアス
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
next-env.d.ts は削除・変更しない
next-env.d.ts は Next.js が自動生成するファイルで、next/imagenext/link などの型を読み込みます。このファイルは .gitignore に入れず、かつ手動で変更してはいけません。Next.js の実行ごとに上書きされます。

App Router の各ファイルの型定義

App Router では app/ ディレクトリ内の特定ファイル名(page.tsxlayout.tsx など)が特別な意味を持ちます。それぞれのファイルが受け取る Props の型を確認しましょう。

ファイル名 役割 受け取る Props
page.tsx ルートのページコンポーネント params(動的ルート)・searchParams(クエリ)
layout.tsx ネストされたレイアウト children: React.ReactNode(必須)・params(動的ルート)
loading.tsx Suspense のフォールバック なし(Props なし)
error.tsx エラーバウンダリ error: Errorreset: () => void
not-found.tsx 404 ページ なし(Props なし)
global-error.tsx ルートレイアウトのエラー error: Error & { digest?: string }reset

page.tsx の型定義

// app/blog/page.tsx(静的ルート)
export default function BlogPage() {
  return <h1>ブログ一覧</h1>;
}

// app/blog/[id]/page.tsx(動的ルート)
// Next.js 14 以降: params は Promise<{ id: string }> に変更
interface PageProps {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function BlogDetailPage({ params, searchParams }: PageProps) {
  const { id } = await params;
  const { page = "1" } = await searchParams;

  const post = await fetchPost(id);
  return <article>{post.title}</article>;
}
Next.js 15 で params / searchParams が Promise 型になった
Next.js 15 以降、paramssearchParamsPromise<...> 型になりました(非同期化)。await params で値を取り出す必要があります。Next.js 14 以前は同期的に { params: { id: string } } でしたが、公式の推奨は async/await 形式への移行です。

layout.tsx の型定義

// app/layout.tsx(ルートレイアウト)
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "My App",
  description: "My Next.js App",
};

interface RootLayoutProps {
  children: React.ReactNode;
}

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  );
}

// app/blog/layout.tsx(セクションレイアウト・動的ルートを含む場合)
interface BlogLayoutProps {
  children: React.ReactNode;
  params: Promise<{ category: string }>;
}

export default async function BlogLayout({ children, params }: BlogLayoutProps) {
  const { category } = await params;
  return (
    <div>
      <nav>カテゴリ: {category}</nav>
      {children}
    </div>
  );
}

error.tsx と loading.tsx の型定義

// app/blog/error.tsx
"use client"; // error.tsx は必ず Client Component

interface ErrorProps {
  error: Error & { digest?: string }; // digest はサーバーエラーのハッシュ
  reset: () => void;                  // エラーバウンダリをリセットする関数
}

export default function ErrorPage({ error, reset }: ErrorProps) {
  return (
    <div>
      <h2>エラーが発生しました: {error.message}</h2>
      <button onClick={reset}>再試行</button>
    </div>
  );
}

// app/blog/loading.tsx(Props なし・シンプルなコンポーネント)
export default function LoadingPage() {
  return <div>読み込み中...</div>;
}

Dynamic Routes の型定義パターン

// ─── 単一パラメータ ───
// app/users/[id]/page.tsx
interface Props { params: Promise<{ id: string }> }

// ─── 複数パラメータ ───
// app/shop/[category]/[id]/page.tsx
interface Props {
  params: Promise<{ category: string; id: string }>;
}

// ─── Catch-all Segments [...slug] ───
// app/docs/[...slug]/page.tsx
interface Props {
  params: Promise<{ slug: string[] }>; // 常に配列
}

// ─── Optional Catch-all [[...slug]] ───
// app/docs/[[...slug]]/page.tsx
interface Props {
  params: Promise<{ slug?: string[] }>; // undefined になる場合がある
}

// ─── searchParams(URL クエリパラメータ)───
// /search?q=typescript&page=2
interface SearchProps {
  searchParams: Promise<{
    q?: string;                  // 単一値
    page?: string;               // 数値は文字列として受け取る
    tags?: string | string[];    // 複数値の場合は配列になる場合も
  }>;
}

export default async function SearchPage({ searchParams }: SearchProps) {
  const { q = "", page = "1" } = await searchParams;
  const currentPage = parseInt(page, 10);
  // ...
}
params の値はすべて string 型
URL パラメータは常に string(または string[])型です。ID を数値として使う場合は parseInt(id, 10) または Number(id) で変換してください。params.idnumber 型と期待するとコンパイルエラーになります。

Server Components と Client Components の型の違い

App Router ではデフォルトがServer Componentです。"use client" ディレクティブを追加した場合だけ Client Component になります。この違いは型定義にも影響します。

// ─── Server Component(デフォルト)───
// async 関数にできる・await でデータフェッチ可
// useState, useEffect, イベントハンドラは使えない

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`https://api.example.com/users/${id}`);
  if (!res.ok) throw new Error("ユーザーの取得に失敗しました");
  return res.json();
}

// Server Component: async 関数 OK
export default async function UserProfile({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const user = await fetchUser(id); // Server Component 内で直接フェッチ

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <LikeButton userId={user.id} />
    </div>
  );
}

// ─── Client Component ───
"use client";

import { useState } from "react";

interface LikeButtonProps {
  userId: number;
  initialCount?: number;
}

// Client Component: useState・イベントハンドラ OK、async 不可
export function LikeButton({ userId, initialCount = 0 }: LikeButtonProps) {
  const [count, setCount] = useState(initialCount);

  const handleLike = async () => {
    setCount((prev) => prev + 1);
    await fetch(`/api/users/${userId}/like`, { method: "POST" });
  };

  return <button onClick={handleLike}>♥ {count}</button>;
}
Server Component から Client Component へ渡せる Props の制約
Server Component から Client Component に渡す Props はシリアライズ可能な値のみです。関数・クラスインスタンス・Date オブジェクト(そのまま)・undefined などは渡せません。例:Datestring(ISO 8601 形式)に変換してから渡してください。Server Actions"use server"関数)は例外的に渡せます。

Route Handlers(API Routes)の型定義

App Router では app/api/ ディレクトリ内の route.ts ファイルが API エンドポイントになります。NextRequest / NextResponse を使って型安全な API を構築できます。

// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

interface User {
  id: number;
  name: string;
  email: string;
}

interface CreateUserRequest {
  name: string;
  email: string;
}

// GET /api/users
export async function GET(request: NextRequest): Promise<NextResponse<User[]>> {
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get("page") ?? "1", 10);

  const users = await db.users.findMany({ skip: (page - 1) * 20, take: 20 });
  return NextResponse.json(users);
}

// POST /api/users
export async function POST(request: NextRequest): Promise<NextResponse<User | { error: string }>> {
  const body = await request.json() as CreateUserRequest;

  if (!body.name || !body.email) {
    return NextResponse.json({ error: "name と email は必須です" }, { status: 400 });
  }

  const user = await db.users.create({ data: body });
  return NextResponse.json(user, { status: 201 });
}
// app/api/users/[id]/route.ts(動的ルートの Route Handler)
import { NextRequest, NextResponse } from "next/server";

interface RouteContext {
  params: Promise<{ id: string }>;
}

// GET /api/users/42
export async function GET(
  request: NextRequest,
  { params }: RouteContext
): Promise<NextResponse<User | { error: string }>> {
  const { id } = await params;
  const userId = parseInt(id, 10);

  if (isNaN(userId)) {
    return NextResponse.json({ error: "無効な ID です" }, { status: 400 });
  }

  const user = await db.users.findUnique({ where: { id: userId } });
  if (!user) return NextResponse.json({ error: "ユーザーが見つかりません" }, { status: 404 });

  return NextResponse.json(user);
}

// DELETE /api/users/42
export async function DELETE(
  _request: NextRequest,
  { params }: RouteContext
): Promise<NextResponse<{ success: boolean }>> {
  const { id } = await params;
  await db.users.delete({ where: { id: parseInt(id, 10) } });
  return NextResponse.json({ success: true });
}
Zod でリクエストボディを型安全に検証する
Route Handler で request.json() の結果は any 型です。Zod を使うとバリデーションと型付けを同時に行えます:
const body = CreateUserSchema.parse(await request.json());
parse に失敗すると ZodError がスローされるため、try/catch で 400 レスポンスを返しましょう。

Server Actions の型定義

Server Actions は "use server" を付けたサーバー側の関数で、Client Component から直接呼び出せます。フォーム送信・データ変更処理を型安全に書けます。

// app/actions/user.ts
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

// Server Action の戻り値型(成功 or エラー)
interface ActionResult {
  success: boolean;
  error?: string;
  fieldErrors?: Record<string, string>;
}

// フォームから呼び出す Server Action
// FormData を受け取る場合の型: (prevState: S, formData: FormData) => Promise<S>
export async function createUser(
  prevState: ActionResult,
  formData: FormData
): Promise<ActionResult> {
  const name  = formData.get("name")  as string | null;
  const email = formData.get("email") as string | null;

  // バリデーション
  const fieldErrors: Record<string, string> = {};
  if (!name?.trim())  fieldErrors.name  = "名前は必須です";
  if (!email?.trim()) fieldErrors.email = "メールは必須です";
  if (Object.keys(fieldErrors).length > 0) return { success: false, fieldErrors };

  try {
    await db.users.create({ data: { name: name!, email: email! } });
    revalidatePath("/users");
    return { success: true };
  } catch {
    return { success: false, error: "ユーザーの作成に失敗しました" };
  }
}
// app/users/new/page.tsx(Server Action を使うフォーム)
"use client";

import { useActionState } from "react"; // React 19 / Next.js 14+
import { createUser } from "@/actions/user";

const initialState = { success: false };

export default function NewUserForm() {
  const [state, formAction, isPending] = useActionState(createUser, initialState);

  return (
    <form action={formAction}>
      <input name="name" placeholder="名前" />
      {state.fieldErrors?.name && <p>{state.fieldErrors.name}</p>}

      <input name="email" type="email" placeholder="メール" />
      {state.fieldErrors?.email && <p>{state.fieldErrors.email}</p>}

      <button type="submit" disabled={isPending}>
        {isPending ? "送信中..." : "作成"}
      </button>

      {state.error && <p style={{ color: "red" }}>{state.error}</p>}
      {state.success && <p style={{ color: "green" }}>作成しました</p>}
    </form>
  );
}
useActionState(旧: useFormState)
React 19 / Next.js 14 以降では useActionStatereact からインポート)を使います。旧来の useFormStatereact-dom)は非推奨になりました。TypeScript の型定義: useActionState<S, P>(action, initialState)

Metadata API の型定義

Next.js の Metadata API は Metadata 型が提供されており、静的・動的メタデータの両方を型安全に定義できます。

import type { Metadata } from "next";

// ─── 静的 Metadata(app/layout.tsx や app/page.tsx に export)───
export const metadata: Metadata = {
  title: "コーディングライフスタイル",
  description: "Web技術の実践的な解説記事",
  keywords: ["TypeScript", "Next.js", "React"],
  authors: [{ name: "Admin", url: "https://example.com" }],
  openGraph: {
    type: "website",
    title: "コーディングライフスタイル",
    description: "Web技術の実践的な解説記事",
    url: "https://codingls.com",
    images: [{ url: "/og-image.png", width: 1200, height: 630 }],
  },
  twitter: {
    card: "summary_large_image",
    title: "コーディングライフスタイル",
  },
};
// ─── 動的 Metadata(generateMetadata 関数)───
import type { Metadata, ResolvingMetadata } from "next";

interface Props {
  params: Promise<{ id: string }>;
}

export async function generateMetadata(
  { params }: Props,
  parent: ResolvingMetadata // 親レイアウトの Metadata にアクセス可能
): Promise<Metadata> {
  const { id } = await params;
  const post = await fetchPost(id);

  // 親の OGP 画像を継承して追加することもできる
  const parentImages = (await parent).openGraph?.images ?? [];

  return {
    title: `${post.title} | My Blog`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      images: [post.ogImage, ...parentImages],
    },
  };
}

generateStaticParams の型定義

generateStaticParams は動的ルートを静的に生成するための関数で、getStaticPaths(Pages Router)に相当します。

// app/blog/[slug]/page.tsx

// generateStaticParams はビルド時に実行される
// 戻り値は { params名: 値 } のオブジェクトの配列
export async function generateStaticParams(): Promise<{ slug: string }[]> {
  const posts = await fetchAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

// 複数パラメータの場合
// app/shop/[category]/[id]/page.tsx
export async function generateStaticParams(): Promise<{
  category: string;
  id: string;
}[]> {
  const products = await fetchAllProducts();
  return products.map((p) => ({
    category: p.category,
    id:       String(p.id),
  }));
}

// Catch-all の場合
// app/docs/[...slug]/page.tsx
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
  return [
    { slug: ["getting-started"] },
    { slug: ["api", "reference"] },
    { slug: ["guides", "typescript", "setup"] },
  ];
}

環境変数の型安全化

process.env.API_URL の型は string | undefined です。直接使うと undefined の可能性がある型エラーが頻発します。Zod を使って環境変数を一元管理するのが最も型安全な方法です。

// ❌ 問題:process.env の値は string | undefined
const apiUrl = process.env.API_URL;          // string | undefined
const timeout = process.env.TIMEOUT;         // string | undefined
fetch(apiUrl + "/users"); // Error: 型 undefined は渡せない場合がある

// ✅ 解決策:Zod で環境変数を検証・型定義
// lib/env.ts
import { z } from "zod";

const envSchema = z.object({
  // サーバーサイドのみ(クライアントに露出しない)
  DATABASE_URL:     z.string().url(),
  API_SECRET:       z.string().min(1),
  NODE_ENV:         z.enum(["development", "test", "production"]),

  // クライアントサイド(NEXT_PUBLIC_ プレフィックスが必要)
  NEXT_PUBLIC_API_URL:   z.string().url(),
  NEXT_PUBLIC_APP_NAME:  z.string().default("My App"),

  // オプション(デフォルト値付き)
  PORT:    z.coerce.number().default(3000),
  TIMEOUT: z.coerce.number().default(5000),
});

// アプリ起動時にバリデーション(失敗すれば即クラッシュ)
export const env = envSchema.parse(process.env);
// → env.DATABASE_URL は string 型(undefined なし)
// → env.PORT は number 型(coerce で変換済み)
// 使用例:lib/env.ts から import して使う
import { env } from "@/lib/env";

async function fetchUsers() {
  const res = await fetch(`${env.NEXT_PUBLIC_API_URL}/users`, {
    headers: { Authorization: `Bearer ${env.API_SECRET}` },
    signal: AbortSignal.timeout(env.TIMEOUT),
  });
  return res.json();
}

// next.config.js でも環境変数を検証できる
// next.config.ts
import { env } from "./src/lib/env"; // ビルド時に検証

const nextConfig = {
  env: {
    NEXT_PUBLIC_API_URL: env.NEXT_PUBLIC_API_URL,
  },
};

export default nextConfig;

Zod の詳しい使い方はTypeScript Zod 完全ガイドを参照してください。

next/image・next/link の型定義

// next/image の型
import Image from "next/image";

// ImageProps: src・alt・width・height(またはfill)は必須
function UserAvatar({ src, name }: { src: string; name: string }) {
  return (
    <Image
      src={src}
      alt={`${name}のアバター`}
      width={64}
      height={64}
      className="rounded-full"
      priority={false}      // LCP 要素なら true
    />
  );
}

// fill を使う場合(親に position: relative が必要)
function HeroImage({ src }: { src: string }) {
  return (
    <div style={{ position: "relative", height: "400px" }}>
      <Image src={src} alt="ヒーロー画像" fill style={{ objectFit: "cover" }} />
    </div>
  );
}

// next/link の型
import Link from "next/link";

// href は string | UrlObject 型
// UrlObject: { pathname, query, hash, ... }
function PostLink({ postId, title }: { postId: number; title: string }) {
  return (
    <Link
      href={{ pathname: "/blog/[id]", query: { id: postId } }}
      // または href={`/blog/${postId}`}
      prefetch={false}  // デフォルト true(ビューポートに入ると先読み)
    >
      {title}
    </Link>
  );
}

next/navigation・next/headers の型定義

// ─── next/navigation ─── (Client Component 内で使用)
"use client";
import { useRouter, usePathname, useSearchParams, useParams } from "next/navigation";

function NavigationExample() {
  const router     = useRouter();       // AppRouterInstance 型
  const pathname   = usePathname();     // string 型
  const params     = useParams();       // Readonly<Record<string, string | string[]>> 型
  const searchParamsHook = useSearchParams(); // ReadonlyURLSearchParams 型

  const handleNavigate = () => {
    router.push("/dashboard");
    router.replace("/login");
    router.refresh(); // Server Component を再実行(キャッシュクリア)
  };

  const query = searchParamsHook.get("q") ?? ""; // string | null → string
  return <div>現在のパス: {pathname}</div>;
}

// ─── next/headers ─── (Server Component / Route Handler 内で使用)
import { cookies, headers } from "next/headers";

async function ServerExample() {
  const cookieStore = await cookies();
  const token = cookieStore.get("auth_token"); // RequestCookie | undefined
  if (!token) return null;

  const headersList = await headers();
  const userAgent = headersList.get("user-agent"); // string | null

  return <div>UA: {userAgent}</div>;
}
next/navigation の Hooks は Client Component 専用
useRouterusePathnameuseSearchParamsuseParamsClient Component("use client")の中でしか使えません。Server Component で useRouter() を使うと「Error: useRouter only works in Client Component」が発生します。Server Component でパスやクエリを取得するには page.tsxparamssearchParams Props を使ってください。

App Router でよくある型エラーと解消法

エラー1:params.id が Promise でないと思ってアクセスする

// ❌ Next.js 15 以降でエラー
export default function Page({ params }: { params: { id: string } }) {
  const { id } = params; // Error: params は Promise 型
  // ...
}

// ✅ await で値を取り出す
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params; // OK
  // ...
}

エラー2:Server Component で useState / useEffect を使う

// ❌ Server Component で React Hooks は使えない
import { useState } from "react"; // Error: useState を Server Component で使用

export default async function Page() {
  const [count, setCount] = useState(0); // 実行時エラー
  // ...
}

// ✅ 解決策1: "use client" を追加して Client Component にする
"use client";
import { useState } from "react";
export default function Page() { ... }

// ✅ 解決策2: useState が必要な部分だけ切り出して Client Component にする
// page.tsx(Server Component のまま)
import { Counter } from "./counter"; // Client Component
export default async function Page() {
  const data = await fetchData();
  return <Counter initialData={data} />;
}

エラー3:Route Handler の response 型エラー

// ❌ NextResponse.json の型推論が合わない場合
export async function GET(): Promise<NextResponse> {
  return NextResponse.json({ users: [] }); // OK だが型情報が弱い
}

// ✅ ジェネリクスで型を明示
interface UsersResponse { users: User[]; total: number; }
export async function GET(): Promise<NextResponse<UsersResponse>> {
  return NextResponse.json({ users: [], total: 0 });
}

// ✅ エラー時は union 型
export async function GET(): Promise<NextResponse<User[] | { error: string }>> {
  try {
    const users = await db.users.findMany();
    return NextResponse.json(users);
  } catch {
    return NextResponse.json({ error: "サーバーエラー" }, { status: 500 });
  }
}

エラー4:children の型エラー

// ❌ children の型が合わない
function Layout({ children }: { children: JSX.Element }) {
  // NG: JSX.Element は React.ReactNode より狭い型
  // null・string・配列・Fragmentは受け取れない
  return <div>{children}</div>;
}

// ✅ React.ReactNode を使う(最も広い型)
function Layout({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>;
}

// React 18+ では React.FC を避け、明示的な Props 型 + 関数宣言が推奨
// React.FC は children を自動で含まなくなった(React 18 の破壊的変更)

よくある質問

QPages Router(pages/ ディレクトリ)と App Router(app/ ディレクトリ)は共存できますか?

Aできます。Next.js は pages/app/同じプロジェクト内で共存できます。移行期間中は同じ URL パスを両方に定義しないよう注意してください(app/ が優先されます)。型定義は全く別体系なので、混在するときは現在どちらのディレクトリにいるかを意識してください。

QServer Component と Client Component を判別する型はありますか?

ATypeScript の型システム上、Server Component と Client Component は区別されません(どちらも通常の React コンポーネント型)。判別は "use client" ディレクティブの有無によります。実行時の制約(Hooks の使用可否など)は TypeScript ではなく Next.js のビルド時チェックによるものです。ESLint の eslint-plugin-react-hooks が Hooks の誤用を検出してくれます。

Qfetch の戻り値を型安全にするにはどうすればよいですか?

Ares.json() の戻り値は any 型です。型アサーション as User[] で型を付けるのが最もシンプルですが、ランタイムで型の保証はされません。Zod を使って UserSchema.array().parse(await res.json()) とすると、バリデーションと型付けを同時に行えます。

Qnext/font の型定義はどう使いますか?

Anext/font/google から関数をインポートして使います。戻り値の型は NextFont で、.classNamestring)を HTML 要素の className に渡します:const inter = Inter({ subsets: ["latin"] }); <body className={inter.className}>。型は Next.js の next-env.d.ts で自動インポートされるため @types の追加は不要です。

QTypeScript で next.config.js を型安全に書くには?

Anext.config.jsnext.config.ts(TypeScript)にリネームし、import type { NextConfig } from "next"; で型をインポートします。その上で const config: NextConfig = { ... }; export default config; とすると tsconfig の型チェックが効くようになります(Next.js 14.1 以降でサポート)。

まとめ

Next.js App Router の TypeScript 型定義は、ファイルごとに決まったパターンがあります。

ファイル / 機能 型定義のポイント
page.tsx params: Promise<{ key: string }>searchParams: Promise<{ ... }>
layout.tsx children: React.ReactNode(必須)・params は動的ルートのみ
error.tsx "use client" 必須・error: Errorreset: () => void
Route Handlers GET/POST/PUT/DELETE(req: NextRequest, { params }): Promise<NextResponse<T>>
Server Actions (prevState: S, formData: FormData): Promise<S>"use server" 必須
Metadata Metadata 型を export / generateMetadata 関数を async で定義
環境変数 Zod で envSchema.parse(process.env) — 型推論+起動時バリデーション

TypeScript React ガイドTypeScript Express ガイドZod 完全ガイドも合わせて参照することで、フルスタック TypeScript 開発の型安全性をさらに高められます。