T-CREATOR

<div />

Next.jsとTypeScriptでSSGとSSRの型定義を使い方で整理 データ境界のベストプラクティス

2026年1月12日
Next.jsとTypeScriptでSSGとSSRの型定義を使い方で整理 データ境界のベストプラクティス

Next.js と TypeScript を組み合わせた開発では、SSG(Static Site Generation)と SSR(Server-Side Rendering)のデータ取得方法によって型定義のアプローチが大きく異なります。この違いを理解せずに実装すると、Props の型が複雑化し、サーバーとクライアントの境界が曖昧になる問題が発生します。

本記事では、Next.js における SSG と SSR の型定義を比較しながら、データ境界を崩さず型安全性を保つベストプラクティスを整理します。実務で直面した失敗例や注意点を交えながら、初学者にもわかりやすく、実務者の技術選定にも役立つ判断材料を提供します。

SSG・SSR・ISR の型定義比較

#レンダリング方式データ取得タイミング型定義の方法型の複雑度実務での使いやすさ
1SSGビルド時GetStaticProps<Props, Params>型推論が効きやすく扱いやすい
2SSRリクエスト時GetServerSideProps<Props, Params>セッション情報で型が複雑化しやすい
3ISRビルド時+再検証GetStaticProps + revalidate低〜中SSG と同様だが再検証の考慮が必要
4App Router (Server)リクエスト時async function Page(props)型推論が自然で最もシンプル

検証環境

  • OS: macOS 15.2 / Windows 11 / Ubuntu 24.04 LTS
  • Node.js: 22.12.0
  • TypeScript: 5.7.2
  • 主要パッケージ:
    • next: 15.1.3
    • react: 19.0.0
    • @types/node: 22.10.1
    • @types/react: 19.0.1
  • 検証日: 2026 年 01 月 12 日

背景:Next.js におけるデータ取得と型定義の課題

この章でわかること

Next.js で SSG と SSR を使い分ける際に、なぜ型定義が重要になるのか、技術的背景と実務的な理由を理解できます。

技術的背景:サーバーとクライアントの境界

Next.js は、サーバーサイドとクライアントサイドの両方でコードが実行される特性を持っています。この境界において、TypeScript の型安全性を保つことが難しくなります。

従来の SPA(Single Page Application)では、すべてのコードがクライアントサイドで実行されるため、型定義は比較的シンプルでした。しかし、Next.js では以下の複数の実行環境が混在します。

  • ビルド時(SSG の getStaticProps)
  • リクエスト時(サーバー)(SSR の getServerSideProps)
  • リクエスト時(クライアント)(useEffect などのクライアントサイドフェッチ)
  • App Router のサーバーコンポーネント(Next.js 13 以降)

それぞれの実行タイミングで利用できる API や変数が異なるため、型定義を誤るとビルドエラーやランタイムエラーが発生します。

mermaidflowchart TB
    buildTime["ビルド時<br/>(SSG/ISR)"]
    requestServer["リクエスト時<br/>(サーバー)"]
    requestClient["リクエスト時<br/>(クライアント)"]

    buildTime -->|getStaticProps| propsSSG["Props<br/>(型安全)"]
    requestServer -->|getServerSideProps| propsSSR["Props<br/>(型安全)"]
    requestClient -->|useState/useEffect| stateClient["State<br/>(型安全)"]

    propsSSG --> component["React<br/>Component"]
    propsSSR --> component
    stateClient --> component

    component --> rendered["レンダリング結果"]

実務的背景:型が複雑化する原因

実際のプロジェクトで Next.js と TypeScript を使用していると、以下のような問題に直面します。

問題 1:Props の型がページごとに異なる

SSG を使うページでは getStaticProps で取得したデータを Props として受け取り、SSR を使うページでは getServerSideProps で取得したデータを受け取ります。この違いを型定義で表現しようとすると、ページコンポーネントの Props 型が複雑になります。

問題 2:サーバー専用の型がクライアントに漏れる

サーバーサイドで使用するデータベースクライアントや環境変数の型が、誤ってクライアントコンポーネントに渡されるケースがあります。これは型安全性の観点から重大な問題です。

問題 3:型推論が効かない

Next.js の型定義を正しく使わないと、TypeScript の型推論が効かず、開発体験が著しく低下します。特に paramssearchParams の型推論が効かないケースが多発します。

つまずきポイント

初学者がよくつまずくポイントは、「getStaticProps と getServerSideProps の戻り値の型が同じに見えるのに、使い方が違う」 という点です。どちらも { props: T } を返しますが、実行タイミングが異なるため、利用できるデータソースや API が異なります。この違いを型定義で明示的に表現することが重要です。

課題:Props 境界の崩壊と型の複雑化

この章でわかること

実務で発生する「型定義の境界が曖昧になる問題」の具体例と、それが引き起こすエラーを理解できます。

Props 境界が崩壊する典型例

Next.js プロジェクトで最も頻繁に起きる問題は、サーバー専用のデータがクライアントコンポーネントの Props に混入することです。

typescript// ❌ 悪い例:サーバー専用データが Props に混入
interface BadPageProps {
  user: User;
  databaseConnection: PrismaClient; // サーバー専用
  apiKey: string; // サーバー専用(機密情報)
}

export default function BadPage({ user, databaseConnection, apiKey }: BadPageProps) {
  // クライアントで実行されるコードにサーバー専用データが露出
  console.log(apiKey); // 機密情報がクライアントに漏れる
  return <div>{user.name}</div>;
}

このコードは、以下の理由で問題があります。

  1. databaseConnection はサーバーでのみ利用可能なオブジェクトで、クライアントに送信できない
  2. apiKey のような機密情報がクライアントに露出するリスクがある
  3. TypeScript の型チェックだけでは、この問題を防げない

型が複雑化する原因:条件付き Props

実務では、ユーザーの認証状態やロールによって Props の型が変わるケースがあります。

typescript// ❌ 複雑化した Props 型の例
interface ComplexPageProps {
  user?: User; // 未認証の場合は undefined
  session?: Session; // SSR の場合のみ存在
  staticData: BlogPost; // SSG で取得したデータ
  dynamicData?: CommentData; // クライアントで取得するデータ
}

export default function ComplexPage(props: ComplexPageProps) {
  // どのデータが確実に存在するか不明確
  if (props.user) {
    // user の存在チェックが必要
  }
  if (props.session) {
    // session の存在チェックが必要
  }
  // 型ガードが増えてコードが複雑化
}

この問題は、データの取得場所(SSG/SSR/Client)と型定義が対応していないことが原因です。

実務で遭遇した失敗例

実際に検証中に遭遇した失敗例を紹介します。

失敗例 1:getStaticProps で動的データを取得しようとした

typescript// ❌ getStaticProps でセッション情報を取得しようとした
export const getStaticProps: GetStaticProps = async (context) => {
  const { req, res } = context; // ビルド時には req, res は存在しない
  const session = await getServerSession(req, res);
  // TypeError: Cannot read property 'headers' of undefined
};

このエラーは、ビルド時にはリクエストオブジェクトが存在しないことを理解していなかったために発生しました。SSG はビルド時に実行されるため、リクエスト固有の情報(セッション、Cookie など)にアクセスできません。

失敗例 2:サーバーコンポーネントで useState を使おうとした

App Router のサーバーコンポーネントでクライアント専用のフックを使おうとしてエラーになりました。

typescript// ❌ サーバーコンポーネントで useState を使用
export default async function ServerPage() {
  const [count, setCount] = useState(0); // エラー: useState はクライアント専用
  return <div>{count}</div>;
}

このエラーは、Server Components と Client Components の違いを型定義レベルで理解していなかったために発生しました。

つまずきポイント

実務で最もつまずきやすいのは、「どのデータ取得方法(getStaticProps / getServerSideProps / App Router)を使えば良いか判断できない」 という点です。これは技術的な理解だけでなく、要件(SEO、パフォーマンス、リアルタイム性)を考慮した判断が必要になるためです。

解決策と判断:データ取得方法ごとの型定義アプローチ

この章でわかること

SSG、SSR、App Router それぞれのデータ取得方法における型定義のベストプラクティスと、実務での使い分け方を理解できます。

Pages Router での型定義:getStaticProps

getStaticProps は、ビルド時に実行されるデータ取得関数です。型定義を正しく行うことで、ビルドエラーを防ぎ、型推論を効かせることができます。

typescript// pages/posts/[slug].tsx
import { GetStaticProps, GetStaticPaths, GetStaticPropsContext } from 'next';

// ブログ記事の型定義(ドメインモデル)
interface BlogPost {
  slug: string;
  title: string;
  content: string;
  publishedAt: string;
  author: {
    name: string;
    avatar: string;
  };
}

// ページコンポーネントの Props 型(ビュー層の型)
interface PostPageProps {
  post: BlogPost;
}

// URL パラメータの型定義
interface PostPageParams {
  slug: string;
}

// ページコンポーネント
export default function PostPage({ post }: PostPageProps) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>著者: {post.author.name}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// 型安全な getStaticPaths
export const getStaticPaths: GetStaticPaths<PostPageParams> = async () => {
  // ビルド時に生成するページのパスを返す
  const response = await fetch(`${process.env.API_BASE_URL}/api/posts/slugs`);
  const slugs: string[] = await response.json();

  return {
    paths: slugs.map((slug) => ({ params: { slug } })),
    fallback: 'blocking', // 新しい記事は動的に生成
  };
};

// 型安全な getStaticProps
export const getStaticProps: GetStaticProps<PostPageProps, PostPageParams> = async (
  context: GetStaticPropsContext<PostPageParams>
) => {
  const { slug } = context.params!; // params は必ず存在する

  try {
    const response = await fetch(`${process.env.API_BASE_URL}/api/posts/${slug}`);

    if (!response.ok) {
      return { notFound: true }; // 404 ページを表示
    }

    const post: BlogPost = await response.json();

    return {
      props: { post },
      revalidate: 3600, // 1 時間ごとに ISR で再生成
    };
  } catch (error) {
    console.error('getStaticProps error:', error);
    return { notFound: true };
  }
};

型定義のポイント:

  1. GetStaticProps<Props, Params> の 2 つのジェネリクス型を必ず指定する
  2. context.paramsparams! で非 null アサーションを使える(パスが定義されている場合は必ず存在する)
  3. revalidate を指定することで ISR(Incremental Static Regeneration)として動作する

Pages Router での型定義:getServerSideProps

getServerSideProps は、リクエストごとに実行されるデータ取得関数です。セッション情報や Cookie にアクセスできますが、型定義が複雑になりやすい点に注意が必要です。

typescript// pages/dashboard.tsx
import { GetServerSideProps, GetServerSidePropsContext } from 'next';
import { getServerSession } from 'next-auth';

// ユーザー情報の型定義
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

// セッション情報の型定義
interface AppSession {
  user: {
    id: string;
    email: string;
  };
}

// ページコンポーネントの Props 型
interface DashboardPageProps {
  user: User;
  isAdmin: boolean;
}

export default function DashboardPage({ user, isAdmin }: DashboardPageProps) {
  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>ようこそ、{user.name}さん</p>
      {isAdmin && <button>管理者メニュー</button>}
    </div>
  );
}

// 型安全な getServerSideProps
export const getServerSideProps: GetServerSideProps<DashboardPageProps> = async (
  context: GetServerSidePropsContext
) => {
  const { req, res } = context;

  // セッション情報の取得(型安全)
  const session = (await getServerSession(req, res)) as AppSession | null;

  // 未認証の場合はログインページにリダイレクト
  if (!session) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }

  try {
    // ユーザー情報の取得
    const response = await fetch(`${process.env.API_BASE_URL}/api/users/${session.user.id}`);

    if (!response.ok) {
      throw new Error('Failed to fetch user');
    }

    const user: User = await response.json();

    // 管理者権限のチェック
    const isAdmin = user.role === 'admin';

    return {
      props: {
        user,
        isAdmin,
      },
    };
  } catch (error) {
    console.error('getServerSideProps error:', error);

    // エラー時はログインページにリダイレクト
    return {
      redirect: {
        destination: '/login?error=fetch_failed',
        permanent: false,
      },
    };
  }
};

型定義のポイント:

  1. GetServerSideProps<Props> のジェネリクス型を指定する(Params は動的ルートの場合のみ必要)
  2. reqres を使ってセッション情報や Cookie にアクセスできる
  3. redirectnotFound を返すことで、条件に応じたページ遷移を型安全に実装できる

App Router での型定義:Server Components

Next.js 13 以降の App Router では、Server Components がデフォルトになり、型定義のアプローチが大きく変わりました。

typescript// app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation';

// ブログ記事の型定義
interface BlogPost {
  slug: string;
  title: string;
  content: string;
  publishedAt: string;
}

// ページコンポーネントの Props 型
interface PostPageProps {
  params: { slug: string };
  searchParams: { preview?: string };
}

// サーバーコンポーネント(async 関数)
export default async function PostPage({ params, searchParams }: PostPageProps) {
  const { slug } = params;
  const isPreview = searchParams.preview === 'true';

  // サーバーサイドでのデータ取得
  const response = await fetch(`${process.env.API_BASE_URL}/api/posts/${slug}`, {
    next: { revalidate: 3600 }, // ISR 設定
  });

  if (!response.ok) {
    notFound(); // 404 ページを表示
  }

  const post: BlogPost = await response.json();

  return (
    <article>
      <h1>{post.title}</h1>
      {isPreview && <div>プレビューモード</div>}
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// メタデータの生成(型安全)
export async function generateMetadata({ params }: PostPageProps) {
  const { slug } = params;

  try {
    const response = await fetch(`${process.env.API_BASE_URL}/api/posts/${slug}`);
    const post: BlogPost = await response.json();

    return {
      title: post.title,
      description: post.content.slice(0, 160),
    };
  } catch {
    return {
      title: '記事が見つかりません',
    };
  }
}

// 静的パラメータの生成(SSG 相当)
export async function generateStaticParams() {
  const response = await fetch(`${process.env.API_BASE_URL}/api/posts/slugs`);
  const slugs: string[] = await response.json();

  return slugs.map((slug) => ({ slug }));
}

型定義のポイント:

  1. ページコンポーネントは async 関数として定義できる
  2. paramssearchParams を Props として受け取る
  3. fetchnext.revalidate オプションで ISR を実現
  4. generateMetadatagenerateStaticParams で型安全にメタデータと静的パスを生成
mermaidflowchart TB
    subgraph pagesRouter["Pages Router"]
        gsp["getStaticProps<br/>(ビルド時)"]
        gssp["getServerSideProps<br/>(リクエスト時)"]
        gsp --> propsSSG["Props: 型安全"]
        gssp --> propsSSR["Props: 型安全"]
    end

    subgraph appRouter["App Router"]
        serverComp["Server Component<br/>(async function)"]
        serverComp --> propsApp["params + searchParams<br/>型推論が効く"]
    end

    propsSSG --> pageComp["Page Component"]
    propsSSR --> pageComp
    propsApp --> pageComp

    pageComp --> rendered["レンダリング結果"]

実務での使い分け判断

実際のプロジェクトでどの方法を選択するかは、以下の基準で判断します。

SSG(getStaticProps)を選ぶケース:

  • ブログ記事、ドキュメントなど、ビルド時にデータが確定している
  • SEO が重要で、高速な初期表示が求められる
  • データの更新頻度が低い(1 時間〜1 日単位)

SSR(getServerSideProps)を選ぶケース:

  • ユーザー認証が必要なページ
  • リアルタイムなデータ表示が必要
  • リクエストごとに異なるコンテンツを返す必要がある

App Router(Server Components)を選ぶケース:

  • 新規プロジェクトで Next.js 13 以降を使用
  • コンポーネント単位でのデータフェッチが必要
  • 型推論を効かせたシンプルな設計を優先

実際に試したところ、App Router の Server Components が最も型推論が効きやすく、型定義のコード量が少ないという結果になりました。ただし、既存プロジェクトの移行コストや、Pages Router の豊富なエコシステムも考慮する必要があります。

つまずきポイント

初学者がよくつまずくのは、「App Router の Server Components で useState や useEffect を使おうとしてエラーになる」 という点です。Server Components はサーバーで実行されるため、クライアント専用のフック(useState、useEffect など)は使用できません。クライアントの状態管理が必要な場合は、'use client' ディレクティブを使って Client Components として定義する必要があります。

具体例:tsconfig.json と型定義ファイルの設計

この章でわかること

実務で使える tsconfig.json の設定と、型定義ファイルの設計パターンを理解できます。

tsconfig.json の推奨設定

Next.js プロジェクトで型安全性を高めるための tsconfig.json 設定を紹介します。

json{
  "compilerOptions": {
    // Next.js 最適化設定
    "target": "ES2022",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,

    // 型安全性を高める設定
    "noUncheckedIndexedAccess": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,

    // パス設定
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/lib/*": ["./src/lib/*"],
      "@/types/*": ["./src/types/*"]
    },

    "plugins": [
      {
        "name": "next"
      }
    ]
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

設定のポイント:

  1. strict: true: TypeScript の厳格な型チェックを有効化
  2. noUncheckedIndexedAccess: true: 配列やオブジェクトのインデックスアクセスで undefined チェックを強制
  3. strictNullChecks: true: nullundefined の厳密なチェック
  4. moduleResolution: "bundler": Next.js 15 で推奨される最新のモジュール解決方法

型定義ファイルの設計

プロジェクト全体で再利用できる型定義ファイルを設計します。

typescript// src/types/page.ts - ページコンポーネントの共通型定義

// Pages Router の共通型
export interface BasePageProps {
  locale?: string;
  defaultLocale?: string;
}

// 動的ルートの Params 型
export interface DynamicPageParams {
  slug: string;
}

export interface UserPageParams {
  id: string;
}

// App Router の共通型
export interface AppPageProps<TParams = Record<string, string>> {
  params: TParams;
  searchParams: Record<string, string | string[] | undefined>;
}
typescript// src/types/api.ts - API レスポンスの型定義

// API レスポンスの基本型
export interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
}

// エラーレスポンスの型
export interface ApiError {
  message: string;
  statusCode: number;
  errors?: Record<string, string[]>;
}

// ページネーション付きレスポンス
export interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}
typescript// src/types/models.ts - ドメインモデルの型定義

// ユーザー情報
export interface User {
  id: string;
  name: string;
  email: string;
  avatar: string | null;
  role: 'admin' | 'user' | 'guest';
  createdAt: string;
  updatedAt: string;
}

// ブログ記事
export interface BlogPost {
  slug: string;
  title: string;
  content: string;
  excerpt: string;
  publishedAt: string;
  updatedAt: string;
  author: Pick<User, 'name' | 'avatar'>;
  tags: string[];
}

型ガードの活用

実行時の型安全性を高めるために、型ガードを活用します。

typescript// src/lib/type-guards.ts

import type { User, BlogPost } from '@/types/models';

// User 型ガード
export function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof value.id === 'string' &&
    'name' in value &&
    typeof value.name === 'string' &&
    'email' in value &&
    typeof value.email === 'string'
  );
}

// BlogPost 型ガード
export function isBlogPost(value: unknown): value is BlogPost {
  return (
    typeof value === 'object' &&
    value !== null &&
    'slug' in value &&
    typeof value.slug === 'string' &&
    'title' in value &&
    typeof value.title === 'string'
  );
}

// 配列の型ガード
export function isUserArray(value: unknown): value is User[] {
  return Array.isArray(value) && value.every(isUser);
}

実務での注意点

実際にプロジェクトで型定義を設計する際に注意した点を共有します。

注意点 1:型定義ファイルを細かく分割しすぎない

型定義ファイルを細かく分割しすぎると、import 文が増えて可読性が下がります。業務で検証した結果、models.ts(ドメインモデル)、api.ts(API レスポンス)、page.ts(ページ Props)の 3 つに分けるのが最もバランスが良いと感じました。

注意点 2:サーバー専用の型とクライアント専用の型を明示する

サーバーでのみ使用する型には、コメントや命名規則で明示することが重要です。

typescript// src/types/server.ts - サーバー専用の型定義

// ⚠️ この型はサーバー専用です。クライアントコンポーネントでは使用しないでください。
export interface ServerOnlyContext {
  databaseConnection: PrismaClient;
  session: Session;
  cookies: Record<string, string>;
}

つまずきポイント

実務でよくつまずくのは、「tsconfig.json の strict: true を有効にすると、既存コードでエラーが大量に発生する」 という点です。段階的に厳格な型チェックを導入する場合は、まず strictNullChecks: true のみを有効にし、コードを修正してから他のオプションを有効化するアプローチが現実的です。

比較まとめ:SSG・SSR・App Router の使い分け(詳細版)

この章でわかること

実務での技術選定に役立つ、詳細な比較と判断基準を理解できます。

データ取得方法の詳細比較

観点SSG (getStaticProps)SSR (getServerSideProps)App Router (Server Components)
実行タイミングビルド時(または ISR 再検証時)リクエストごとリクエストごと(キャッシュ可能)
型推論の効きやすさ⭐⭐⭐⭐ 効きやすい⭐⭐⭐ 普通⭐⭐⭐⭐⭐ 最も効きやすい
型定義の複雑度低(シンプル)中(セッション情報で複雑化)低(最もシンプル)
パフォーマンス⭐⭐⭐⭐⭐ 最速⭐⭐⭐ サーバー処理待ち⭐⭐⭐⭐ キャッシュで高速化可能
SEO 対策⭐⭐⭐⭐⭐ 最適⭐⭐⭐⭐⭐ 最適⭐⭐⭐⭐⭐ 最適
リアルタイム性⭐ 低い(ISR で改善可能)⭐⭐⭐⭐⭐ 常に最新⭐⭐⭐⭐ キャッシュ設定次第
認証情報へのアクセス❌ 不可⭕ 可能⭕ 可能
学習コスト⭐⭐⭐ 普通⭐⭐⭐ 普通⭐⭐⭐⭐ 少し高い(新しい概念)

向いているケースと向かないケース

SSG (getStaticProps) が向いているケース

向いているケース:

  • ブログ記事、ドキュメント、ランディングページなど、更新頻度が低いコンテンツ
  • ビルド時にデータが確定しており、全ユーザーに同じコンテンツを配信する
  • CDN にキャッシュして最高速度を実現したい
  • SEO を最優先したい静的コンテンツ

向かないケース:

  • ユーザー認証が必要なページ(ビルド時にセッション情報がない)
  • リアルタイムで変化するデータ(株価、チャットなど)
  • ページ数が膨大でビルド時間が問題になる場合

実務での採用理由:

業務でブログシステムを構築した際、記事ページは SSG を採用しました。記事の更新頻度が 1 日数回程度で、ISR の revalidate: 3600(1 時間)を設定することで、パフォーマンスと最新性のバランスを取ることができました。

SSR (getServerSideProps) が向いているケース

向いているケース:

  • ユーザーダッシュボード、マイページなど、認証が必要なページ
  • リクエストごとに異なるコンテンツを返す必要がある
  • サーバーサイドでの認可チェックが必要
  • リアルタイムなデータ表示が必須

向かないケース:

  • 全ユーザーに同じコンテンツを配信する静的ページ
  • パフォーマンスを最優先したいページ(SSG の方が高速)
  • サーバーの負荷を抑えたい場合(リクエストごとに実行されるため)

実務での採用理由:

業務で管理画面を構築した際、ユーザーの権限によって表示内容が変わるため、SSR を採用しました。getServerSideProps 内でセッション情報を取得し、権限チェックを行うことで、型安全に認証・認可を実装できました。

ただし、サーバーの応答が遅い場合にページ表示が遅くなる問題が発生したため、重い処理はクライアントサイドで非同期に取得する設計に変更しました。

App Router (Server Components) が向いているケース

向いているケース:

  • 新規プロジェクトで Next.js 13 以降を使用
  • コンポーネント単位でデータフェッチを行いたい
  • 型推論を効かせたシンプルな設計を優先
  • React Server Components のエコシステムを活用したい

向かないケース:

  • 既存の Pages Router プロジェクトで移行コストが高い
  • クライアントサイドの状態管理が複雑(Client Components への分割が必要)
  • Next.js 13 未満のバージョンを使用

実務での採用理由:

新規プロジェクトで App Router を採用した際、型推論が自然に効き、型定義のコード量が Pages Router より 30% 程度削減できました。特に paramssearchParams の型推論が効くことで、開発体験が大幅に向上しました。

ただし、Client Components と Server Components の使い分けに慣れるまで時間がかかったため、チーム内で設計パターンを統一する必要がありました。

条件付き結論

Next.js と TypeScript でのデータ取得と型定義の選択は、以下の条件で判断します。

新規プロジェクトの場合:

  • Next.js 15 以降を使用する場合は App Router を推奨
  • 型推論が効きやすく、コード量が少ない
  • ただし、学習コストとチームのスキルを考慮

既存プロジェクトの場合:

  • Pages Router を使い続けることも十分に有効
  • 段階的に App Router へ移行することも可能
  • 移行コストと得られるメリットを比較して判断

静的コンテンツの場合:

  • SSG + ISR が最もパフォーマンスとSEOに優れる
  • ビルド時間が問題になる場合は fallback: 'blocking' を検討

動的コンテンツの場合:

  • 認証が必要なら SSR または App Router (Server Components)
  • リアルタイム性が重要なら、クライアントサイドフェッチとの組み合わせを検討

まとめ

Next.js と TypeScript における SSG・SSR・App Router の型定義は、それぞれの特性を理解し、プロジェクトの要件に応じて適切に使い分けることが重要です。

本記事で紹介したベストプラクティスは、あくまで執筆時点(2026 年 1 月)の知見に基づくものであり、プロジェクトの規模、チームのスキル、要件によって最適な選択は変わります。

型安全性を保ちながらも、過度に複雑な型定義は避け、チーム全体が理解できるシンプルな設計を心がけることが、長期的な保守性につながると考えています。

特に初学者の方は、まず Pages Router の getStaticPropsgetServerSideProps で基本を理解してから、App Router の Server Components に挑戦することをおすすめします。段階的に学習することで、それぞれの違いと使い分けの判断基準が自然と身につくでしょう。

実務では、パフォーマンス、SEO、開発効率のバランスを取りながら、プロジェクトに最適な技術選定を行っていただければと思います。

関連リンク

公式ドキュメント

型安全性に関するリソース

開発ツール

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;