T-CREATOR

Next.js × TypeScript:SSG・SSR での型定義ベストプラクティス

Next.js × TypeScript:SSG・SSR での型定義ベストプラクティス

Next.js と TypeScript を組み合わせた SSR(Server-Side Rendering)・SSG(Static Site Generation)開発では、型安全性を保ちながら高性能なアプリケーションを構築できますが、同時に多くの型定義の課題に直面します。

特に、サーバーサイドとクライアントサイドで異なる実行環境を持つ Next.js では、従来のクライアントサイドのみの TypeScript 開発とは異なるアプローチが必要になります。本記事では、Next.js × TypeScript での SSR・SSG 開発における型定義のベストプラクティスを、実際のエラーコードとその解決方法を含めて詳しく解説いたします。

Pages Router から App Router への移行期にある現在、両方のアプローチに対応した型定義手法を習得することで、モダンな Next.js アプリケーション開発を効率的に進められるでしょう。

基本設定:TypeScript 環境構築と Next.js 連携

Next.js プロジェクトで TypeScript を活用するための環境構築から始めていきます。適切な設定により、開発体験と型安全性を大幅に向上させることができます。

プロジェクト初期化と必要パッケージ

新規 Next.js プロジェクトを TypeScript で開始する場合の推奨設定です。

typescript// yarn create next-app コマンドでの初期化
yarn create next-app my-nextjs-app --typescript --tailwind --eslint --app

// 既存プロジェクトに TypeScript を追加する場合
yarn add --dev typescript @types/react @types/node @types/react-dom

tsconfig.json の最適化設定

Next.js に特化した TypeScript 設定により、SSR・SSG での型安全性を向上させます。

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

    // SSR/SSG 対応のための型チェック強化
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": 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"]
}

型定義ファイルの構成

プロジェクト全体で一貫した型定義を行うためのファイル構成です。

typescript// src/types/global.d.ts - グローバル型定義
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      readonly NODE_ENV:
        | 'development'
        | 'production'
        | 'test';
      readonly NEXT_PUBLIC_API_URL: string;
      readonly DATABASE_URL: string;
      readonly NEXT_PUBLIC_APP_URL: string;
    }
  }
}

export {};

// src/types/api.ts - API関連の型定義
export interface ApiResponse<T = any> {
  data: T;
  message: string;
  success: boolean;
  timestamp: string;
}

export interface PaginatedResponse<T>
  extends ApiResponse<T[]> {
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

export interface ApiError {
  message: string;
  statusCode: number;
  details?: Record<string, any>;
}

// src/types/page.ts - ページプロップス型定義
export interface BasePageProps {
  params: Record<string, string>;
  searchParams: Record<
    string,
    string | string[] | undefined
  >;
}

export interface ServerSideProps<T = any> {
  props: T;
}

export interface StaticProps<T = any> {
  props: T;
  revalidate?: number | false;
  notFound?: boolean;
  redirect?: {
    destination: string;
    permanent: boolean;
  };
}

よくあるエラーと解決方法を見ていきましょう。

typescript// エラー例: TS2307: Cannot find module 'next' or its corresponding type declarations
// 解決方法: next-env.d.ts ファイルの確認と再生成
// next-env.d.ts
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// このファイルが存在しない場合は yarn dev を実行して自動生成

GetServerSideProps/GetStaticProps 型定義マスター

Pages Router での SSR・SSG 実装において、型安全性を保つための重要なポイントを解説します。

GetServerSideProps の型安全な実装

GetServerSideProps では、サーバーサイドで実行される処理の型定義が重要です。

typescript// pages/users/[id].tsx
import {
  GetServerSideProps,
  GetServerSidePropsContext,
} from 'next';

// ユーザー情報の型定義
interface User {
  id: string;
  name: string;
  email: string;
  avatar: string | null;
  createdAt: string;
  updatedAt: string;
}

// ページプロップスの型定義
interface UserPageProps {
  user: User;
  relatedUsers: User[];
  isOwner: boolean;
}

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

export default function UserPage({
  user,
  relatedUsers,
  isOwner,
}: UserPageProps) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      {isOwner && <button>プロフィール編集</button>}
      {/* 関連ユーザーの表示 */}
      <div>
        <h2>関連ユーザー</h2>
        {relatedUsers.map((relatedUser) => (
          <div key={relatedUser.id}>{relatedUser.name}</div>
        ))}
      </div>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps<
  UserPageProps,
  UserPageParams
> = async (
  context: GetServerSidePropsContext<UserPageParams>
) => {
  const { id } = context.params!; // パラメータは必ず存在することが保証される
  const { req, res } = context;

  try {
    // 並列でデータを取得(型安全)
    const [userResponse, relatedUsersResponse] =
      await Promise.all([
        fetch(
          `${process.env.API_BASE_URL}/api/users/${id}`
        ),
        fetch(
          `${process.env.API_BASE_URL}/api/users/${id}/related`
        ),
      ]);

    // レスポンスの型チェック
    if (!userResponse.ok) {
      if (userResponse.status === 404) {
        return {
          notFound: true,
        };
      }
      throw new Error(
        `Failed to fetch user: ${userResponse.statusText}`
      );
    }

    const user: User = await userResponse.json();
    const relatedUsers: User[] =
      await relatedUsersResponse.json();

    // セッション情報から所有者チェック(型安全)
    const session = await getServerSession(req, res);
    const isOwner = session?.user?.id === user.id;

    return {
      props: {
        user,
        relatedUsers,
        isOwner,
      },
    };
  } catch (error) {
    console.error('GetServerSideProps error:', error);

    return {
      notFound: true,
    };
  }
};

GetStaticProps の型安全な実装

GetStaticProps では、ビルド時の型チェックとランタイムエラーの防止が重要です。

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

// ブログ記事の型定義
interface BlogPost {
  slug: string;
  title: string;
  content: string;
  excerpt: string;
  publishedAt: string;
  updatedAt: string;
  author: {
    name: string;
    avatar: string;
  };
  tags: string[];
  readingTime: number;
}

// ページプロップスの型定義
interface BlogPostPageProps {
  post: BlogPost;
  relatedPosts: BlogPost[];
  totalViews: number;
}

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

export default function BlogPostPage({
  post,
  relatedPosts,
  totalViews,
}: BlogPostPageProps) {
  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <p>
          投稿日:{' '}
          {new Date(post.publishedAt).toLocaleDateString(
            'ja-JP'
          )}
        </p>
        <p>読了時間: {post.readingTime}分</p>
        <p>閲覧数: {totalViews.toLocaleString()}回</p>
      </header>

      <div
        dangerouslySetInnerHTML={{ __html: post.content }}
      />

      <aside>
        <h2>関連記事</h2>
        {relatedPosts.map((relatedPost) => (
          <div key={relatedPost.slug}>
            <h3>{relatedPost.title}</h3>
            <p>{relatedPost.excerpt}</p>
          </div>
        ))}
      </aside>
    </article>
  );
}

export const getStaticPaths: GetStaticPaths<
  BlogPostParams
> = async () => {
  try {
    const response = await fetch(
      `${process.env.API_BASE_URL}/api/posts/slugs`
    );

    if (!response.ok) {
      throw new Error(
        `Failed to fetch post slugs: ${response.statusText}`
      );
    }

    const slugs: string[] = await response.json();

    const paths = slugs.map((slug) => ({
      params: { slug },
    }));

    return {
      paths,
      fallback: 'blocking', // 新しい記事は動的に生成
    };
  } catch (error) {
    console.error('GetStaticPaths error:', error);

    return {
      paths: [],
      fallback: 'blocking',
    };
  }
};

export const getStaticProps: GetStaticProps<
  BlogPostPageProps,
  BlogPostParams
> = async (
  context: GetStaticPropsContext<BlogPostParams>
) => {
  const { slug } = context.params!;

  try {
    // 並列でデータを取得
    const [
      postResponse,
      relatedPostsResponse,
      viewsResponse,
    ] = await Promise.all([
      fetch(
        `${process.env.API_BASE_URL}/api/posts/${slug}`
      ),
      fetch(
        `${process.env.API_BASE_URL}/api/posts/${slug}/related`
      ),
      fetch(
        `${process.env.API_BASE_URL}/api/posts/${slug}/views`
      ),
    ]);

    if (!postResponse.ok) {
      if (postResponse.status === 404) {
        return {
          notFound: true,
        };
      }
      throw new Error(
        `Failed to fetch post: ${postResponse.statusText}`
      );
    }

    const post: BlogPost = await postResponse.json();
    const relatedPosts: BlogPost[] =
      await relatedPostsResponse.json();
    const { views }: { views: number } =
      await viewsResponse.json();

    return {
      props: {
        post,
        relatedPosts,
        totalViews: views,
      },
      revalidate: 3600, // 1時間ごとに再生成
    };
  } catch (error) {
    console.error('GetStaticProps error:', error);

    return {
      notFound: true,
    };
  }
};

エラーハンドリングと型安全性

よくあるエラーとその対処法を確認していきます。

typescript// ❌ よくあるエラー例
// Error: TS2322: Type 'string | string[]' is not assignable to type 'string'
export const getServerSideProps: GetServerSideProps =
  async (context) => {
    const id = context.query.id; // string | string[] | undefined

    // このままでは型エラーが発生
    const user = await fetchUser(id); // fetchUser expects string
  };

// ✅ 正しい実装
export const getServerSideProps: GetServerSideProps<
  UserPageProps
> = async (context) => {
  const { id } = context.params as { id: string }; // 型アサーション

  // または より安全な方法
  if (typeof context.params?.id !== 'string') {
    return {
      notFound: true,
    };
  }

  const user = await fetchUser(context.params.id);

  return {
    props: {
      user,
    },
  };
};

リダイレクトと notFound の型安全な実装

typescript// 型安全なリダイレクト処理
export const getServerSideProps: GetServerSideProps =
  async (context) => {
    const session = await getServerSession(
      context.req,
      context.res
    );

    // 未認証ユーザーのリダイレクト
    if (!session) {
      return {
        redirect: {
          destination:
            '/login?redirect=' +
            encodeURIComponent(context.resolvedUrl),
          permanent: false,
        },
      };
    }

    // 権限チェック
    const user = await getUserById(session.user.id);
    if (!user || user.role !== 'admin') {
      return {
        redirect: {
          destination: '/unauthorized',
          permanent: false,
        },
      };
    }

    return {
      props: {
        user,
      },
    };
  };

// notFound の適切な使用
export const getStaticProps: GetStaticProps = async (
  context
) => {
  const { slug } = context.params!;

  try {
    const post = await getPostBySlug(slug as string);

    // 記事が存在しない場合
    if (!post) {
      return {
        notFound: true,
      };
    }

    // 下書き記事の場合(本番環境)
    if (
      post.status === 'draft' &&
      process.env.NODE_ENV === 'production'
    ) {
      return {
        notFound: true,
      };
    }

    return {
      props: {
        post,
      },
      revalidate: 3600,
    };
  } catch (error) {
    console.error('Error fetching post:', error);
    return {
      notFound: true,
    };
  }
};

App Router 時代の Server Components 型安全性

Next.js 13 以降の App Router では、Server Components が導入され、TypeScript での型定義アプローチが大きく変わりました。従来の Pages Router とは異なる新しい型安全性の確保方法を詳しく見ていきます。

Server Components の基本型定義

App Router では、すべてのコンポーネントがデフォルトで Server Components として動作します。

typescript// app/users/[id]/page.tsx - Server Component
import { notFound } from 'next/navigation';

// パラメータの型定義
interface UserPageParams {
  id: string;
}

interface UserPageSearchParams {
  tab?: 'profile' | 'posts' | 'settings';
  page?: string;
}

// Props の型定義
interface UserPageProps {
  params: UserPageParams;
  searchParams: UserPageSearchParams;
}

// Server Component での非同期データフェッチ
async function getUser(id: string) {
  const response = await fetch(
    `${process.env.API_BASE_URL}/api/users/${id}`,
    {
      next: { revalidate: 3600 }, // ISR設定
    }
  );

  if (!response.ok) {
    if (response.status === 404) {
      notFound(); // 404ページへリダイレクト
    }
    throw new Error(
      `Failed to fetch user: ${response.statusText}`
    );
  }

  return response.json() as Promise<User>;
}

async function getUserPosts(
  userId: string,
  page: number = 1
) {
  const response = await fetch(
    `${process.env.API_BASE_URL}/api/users/${userId}/posts?page=${page}`,
    {
      next: { revalidate: 1800 }, // 30分キャッシュ
    }
  );

  if (!response.ok) {
    throw new Error(
      `Failed to fetch user posts: ${response.statusText}`
    );
  }

  return response.json() as Promise<
    PaginatedResponse<BlogPost>
  >;
}

// メインのページコンポーネント
export default async function UserPage({
  params,
  searchParams,
}: UserPageProps) {
  const { id } = params;
  const { tab = 'profile', page = '1' } = searchParams;

  try {
    // 並列でデータを取得
    const [user, userPosts] = await Promise.all([
      getUser(id),
      tab === 'posts'
        ? getUserPosts(id, parseInt(page))
        : Promise.resolve(null),
    ]);

    return (
      <div className='user-page'>
        <UserHeader user={user} />

        <UserTabs currentTab={tab} userId={id} />

        {tab === 'profile' && <UserProfile user={user} />}
        {tab === 'posts' && userPosts && (
          <UserPosts
            posts={userPosts.data}
            pagination={userPosts.pagination}
          />
        )}
        {tab === 'settings' && <UserSettings user={user} />}
      </div>
    );
  } catch (error) {
    console.error('User page error:', error);
    throw error; // Error Boundary でキャッチされる
  }
}

// メタデータの生成(型安全)
export async function generateMetadata({
  params,
}: UserPageProps) {
  try {
    const user = await getUser(params.id);

    return {
      title: `${user.name} - ユーザープロフィール`,
      description: `${user.name}さんのプロフィールページです。`,
      openGraph: {
        title: user.name,
        description: `${user.name}さんのプロフィール`,
        images: user.avatar ? [user.avatar] : [],
      },
    };
  } catch (error) {
    return {
      title: 'ユーザーが見つかりません',
      description: '指定されたユーザーは存在しません。',
    };
  }
}

// 静的パラメータの生成
export async function generateStaticParams(): Promise<
  UserPageParams[]
> {
  try {
    const response = await fetch(
      `${process.env.API_BASE_URL}/api/users/public-ids`
    );

    if (!response.ok) {
      console.error(
        'Failed to fetch user IDs for static generation'
      );
      return [];
    }

    const userIds: string[] = await response.json();

    return userIds.map((id) => ({
      id,
    }));
  } catch (error) {
    console.error('Error in generateStaticParams:', error);
    return [];
  }
}

Layout とテンプレートの型定義

App Router では、レイアウトとテンプレートの型定義も重要です。

typescript// app/dashboard/layout.tsx
import { ReactNode } from 'react';

interface DashboardLayoutProps {
  children: ReactNode;
  params: {
    // 動的セグメントがある場合のパラメータ
  };
}

// Server Component としてのレイアウト
export default async function DashboardLayout({
  children,
}: DashboardLayoutProps) {
  // サーバーサイドでのユーザー認証チェック
  const session = await getServerSession();

  if (!session) {
    redirect('/login');
  }

  // ユーザー情報の取得
  const user = await getCurrentUser(session.user.id);

  return (
    <div className='dashboard-layout'>
      <DashboardSidebar user={user} />
      <main className='dashboard-main'>
        <DashboardHeader user={user} />
        <div className='dashboard-content'>{children}</div>
      </main>
    </div>
  );
}

// app/dashboard/template.tsx - より細かい制御が必要な場合
interface DashboardTemplateProps {
  children: ReactNode;
}

export default function DashboardTemplate({
  children,
}: DashboardTemplateProps) {
  return (
    <div className='dashboard-template'>
      {/* テンプレート固有のラッパー */}
      <div className='dashboard-wrapper'>{children}</div>
    </div>
  );
}

Client Components での型定義

Server Components から Client Components にデータを渡す際の型安全性も重要です。

typescript// app/dashboard/user-profile-client.tsx
'use client';

import { useState, useEffect } from 'react';

interface UserProfileClientProps {
  initialUser: User; // Server Component から渡される初期データ
  userId: string;
}

export default function UserProfileClient({
  initialUser,
  userId,
}: UserProfileClientProps) {
  const [user, setUser] = useState<User>(initialUser);
  const [isEditing, setIsEditing] = useState(false);
  const [isLoading, setIsLoading] = useState(false);

  // クライアントサイドでの更新処理
  const updateUser = async (updates: Partial<User>) => {
    setIsLoading(true);

    try {
      const response = await fetch(`/api/users/${userId}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(updates),
      });

      if (!response.ok) {
        throw new Error(
          `Failed to update user: ${response.statusText}`
        );
      }

      const updatedUser: User = await response.json();
      setUser(updatedUser);
      setIsEditing(false);
    } catch (error) {
      console.error('Update user error:', error);
      // エラーハンドリング
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className='user-profile-client'>
      {isEditing ? (
        <UserEditForm
          user={user}
          onSave={updateUser}
          onCancel={() => setIsEditing(false)}
          isLoading={isLoading}
        />
      ) : (
        <UserDisplay
          user={user}
          onEdit={() => setIsEditing(true)}
        />
      )}
    </div>
  );
}

エラーハンドリングとローディング状態

App Router では、エラーと読み込み状態の型定義も重要です。

typescript// app/dashboard/error.tsx
'use client';

interface ErrorPageProps {
  error: Error & { digest?: string };
  reset: () => void;
}

export default function DashboardError({
  error,
  reset,
}: ErrorPageProps) {
  useEffect(() => {
    // エラーログの送信
    console.error('Dashboard error:', error);
  }, [error]);

  return (
    <div className='error-page'>
      <h2>エラーが発生しました</h2>
      <p>
        申し訳ございませんが、予期しないエラーが発生いたしました。
      </p>
      <details className='error-details'>
        <summary>エラー詳細</summary>
        <pre>{error.message}</pre>
      </details>
      <button onClick={reset}>再試行</button>
    </div>
  );
}

// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className='loading-page'>
      <div className='loading-spinner' />
      <p>読み込み中...</p>
    </div>
  );
}

// app/dashboard/not-found.tsx
export default function DashboardNotFound() {
  return (
    <div className='not-found-page'>
      <h2>ページが見つかりません</h2>
      <p>お探しのページは存在しません。</p>
      <a href='/dashboard'>ダッシュボードに戻る</a>
    </div>
  );
}

パフォーマンス最適化:型推論の効率化

TypeScript のコンパイル時間とランタイムパフォーマンスの両方を最適化することで、開発体験とユーザー体験を向上させます。

TypeScript コンパイル最適化

Next.js プロジェクトでの TypeScript コンパイルを効率化する設定と手法を解説します。

typescript// tsconfig.json - パフォーマンス最適化設定
{
  "compilerOptions": {
    // 基本設定
    "target": "es2020",
    "lib": ["dom", "dom.iterable", "es6", "es2020"],
    "module": "esnext",
    "moduleResolution": "bundler",

    // パフォーマンス最適化
    "incremental": true,
    "composite": false,
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,

    // 型チェック最適化
    "assumeChangesOnlyAffectDirectDependencies": true,
    "disableReferencedProjectLoad": true,

    // Tree Shaking 最適化
    "moduleDetection": "force",
    "allowSyntheticDefaultImports": true,
    "verbatimModuleSyntax": false,

    // 型推論最適化
    "noUncheckedIndexedAccess": false, // 必要な場合のみ有効化
    "exactOptionalPropertyTypes": false, // 段階的に導入

    // パス解決最適化
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/types/*": ["./src/types/*"],
      "@/lib/*": ["./src/lib/*"],
      "@/components/*": ["./src/components/*"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    ".next",
    "out",
    "dist"
  ],
  // TypeScript プロジェクト参照(大規模プロジェクト用)
  "references": []
}

効率的な型定義パターン

型推論を最適化し、コンパイル時間を短縮する型定義パターンを実装します。

typescript// lib/types/optimized.ts - 効率的な型定義

// 1. 条件型よりもユニオン型を優先
// ❌ 複雑な条件型(コンパイル時間が長い)
type ComplexConditional<T> = T extends string
  ? string[]
  : T extends number
  ? number[]
  : T extends boolean
  ? boolean[]
  : never;

// ✅ シンプルなユニオン型(コンパイル時間が短い)
type SimpleUnion = string[] | number[] | boolean[];

// 2. 再帰型の深度制限
type DeepNested<
  T,
  Depth extends number = 5
> = Depth extends 0
  ? T
  : T extends object
  ? { [K in keyof T]: DeepNested<T[K], Subtract<Depth, 1>> }
  : T;

// 数値減算のヘルパー型
type Subtract<
  T extends number,
  U extends number
> = T extends U ? 0 : T extends 0 ? never : Subtract<T, 1>;

// 3. インデックスアクセス型の最適化
interface OptimizedUser {
  readonly id: string;
  readonly name: string;
  readonly email: string;
  readonly profile: {
    readonly bio: string;
    readonly avatar: string;
  };
}

// ❌ 深いインデックスアクセス
type UserBio = OptimizedUser['profile']['bio'];

// ✅ 中間型を定義
type UserProfile = OptimizedUser['profile'];
type UserBio2 = UserProfile['bio'];

// 4. Mapped Types の最適化
// ❌ 複雑な Mapped Type
type ComplexMapped<T> = {
  [K in keyof T]: T[K] extends string
    ? `prefix_${T[K]}`
    : T[K] extends number
    ? T[K] | string
    : T[K];
};

// ✅ シンプルな Mapped Type
type SimpleMapped<T> = {
  readonly [K in keyof T]: T[K];
};

// 5. 型ガードの効率化
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isUser(value: unknown): value is OptimizedUser {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value &&
    'email' in value
  );
}

// 6. 高次コンポーネントの型定義最適化
interface WithLoadingProps {
  isLoading: boolean;
}

// ❌ 複雑な高次コンポーネント型
type ComplexHOC<P> = (
  Component: React.ComponentType<P>
) => React.ComponentType<P & WithLoadingProps>;

// ✅ シンプルな高次コンポーネント型
type SimpleHOC<P = {}> = React.ComponentType<
  P & WithLoadingProps
>;

function withLoading<P extends object>(
  Component: React.ComponentType<P>
): React.ComponentType<P & WithLoadingProps> {
  return function WithLoadingComponent(props) {
    const { isLoading, ...componentProps } = props;

    if (isLoading) {
      return <div>Loading...</div>;
    }

    return <Component {...(componentProps as P)} />;
  };
}

Bundle サイズの最適化

TypeScript の型情報がバンドルサイズに影響しないよう最適化します。

typescript// next.config.js - バンドル最適化設定
/** @type {import('next').NextConfig} */
const nextConfig = {
  // TypeScript 設定
  typescript: {
    // 本番ビルド時の型チェックをスキップ(CI/CDで別途実行)
    ignoreBuildErrors: process.env.CI === 'true',
  },

  // SWC 最適化(TypeScript トランスパイル高速化)
  swcMinify: true,

  // 実験的機能
  experimental: {
    // TypeScript plugin の最適化
    typedRoutes: true, // 型安全なルーティング
    optimizePackageImports: [
      '@heroicons/react',
      'lucide-react',
      'date-fns',
    ],
  },

  // Webpack 最適化
  webpack: (config, { dev, isServer }) => {
    // 開発時のみ型チェックを並列実行
    if (dev && !isServer) {
      const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
      config.plugins.push(
        new ForkTsCheckerWebpackPlugin({
          typescript: {
            configFile: 'tsconfig.json',
            memoryLimit: 4096,
          },
        })
      );
    }

    // Tree shaking の最適化
    config.optimization.usedExports = true;
    config.optimization.sideEffects = false;

    return config;
  },

  // 出力最適化
  output: 'standalone', // Docker デプロイメント最適化

  // 画像最適化
  images: {
    formats: ['image/webp', 'image/avif'],
    deviceSizes: [
      640, 750, 828, 1080, 1200, 1920, 2048, 3840,
    ],
  },
};

module.exports = nextConfig;

ランタイムパフォーマンス最適化

型安全性を保ちながらランタイムパフォーマンスを向上させる手法です。

typescript// lib/performance/memoization.ts - メモ化による最適化

import { useMemo, useCallback } from 'react';

// 型安全なメモ化フック
export function useTypedMemo<T>(
  factory: () => T,
  deps: React.DependencyList
): T {
  return useMemo(factory, deps);
}

export function useTypedCallback<
  T extends (...args: any[]) => any
>(callback: T, deps: React.DependencyList): T {
  return useCallback(callback, deps);
}

// API レスポンスのメモ化
const apiCache = new Map<string, any>();

export function memoizedApiCall<T>(
  key: string,
  apiCall: () => Promise<T>,
  ttl: number = 5 * 60 * 1000 // 5分
): Promise<T> {
  const cached = apiCache.get(key);

  if (cached && Date.now() - cached.timestamp < ttl) {
    return Promise.resolve(cached.data);
  }

  return apiCall().then((data) => {
    apiCache.set(key, {
      data,
      timestamp: Date.now(),
    });
    return data;
  });
}

// 型安全な React.lazy
export function typedLazy<
  T extends React.ComponentType<any>
>(
  factory: () => Promise<{ default: T }>
): React.LazyExoticComponent<T> {
  return React.lazy(factory);
}

// 使用例
const LazyUserProfile = typedLazy(
  () => import('@/components/UserProfile')
);

// コンポーネントの最適化例
interface OptimizedUserListProps {
  users: User[];
  onUserClick: (userId: string) => void;
}

export const OptimizedUserList =
  React.memo<OptimizedUserListProps>(
    ({ users, onUserClick }) => {
      // 重い計算のメモ化
      const sortedUsers = useTypedMemo(
        () =>
          users.sort((a, b) =>
            a.name.localeCompare(b.name)
          ),
        [users]
      );

      // コールバックのメモ化
      const handleUserClick = useTypedCallback(
        (userId: string) => onUserClick(userId),
        [onUserClick]
      );

      return (
        <div className='user-list'>
          {sortedUsers.map((user) => (
            <UserCard
              key={user.id}
              user={user}
              onClick={handleUserClick}
            />
          ))}
        </div>
      );
    }
  );

OptimizedUserList.displayName = 'OptimizedUserList';

型チェックの段階的最適化

プロジェクトの成長に合わせて段階的に型安全性を向上させる戦略です。

typescript// lib/types/progressive.ts - 段階的型安全化

// レベル1: 基本的な型安全性
interface BasicUser {
  id: string;
  name: string;
  email: string;
}

// レベル2: より厳密な型定義
interface StrictUser {
  readonly id: string;
  readonly name: string;
  readonly email: string;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}

// レベル3: ブランド型による型安全性強化
type UserId = string & { readonly __brand: 'UserId' };
type Email = string & { readonly __brand: 'Email' };

interface BrandedUser {
  readonly id: UserId;
  readonly name: string;
  readonly email: Email;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}

// ブランド型のコンストラクタ
function createUserId(id: string): UserId {
  if (!id || id.length === 0) {
    throw new Error('Invalid user ID');
  }
  return id as UserId;
}

function createEmail(email: string): Email {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    throw new Error('Invalid email format');
  }
  return email as Email;
}

// レベル4: 関数型アプローチでの型安全性
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

async function safeApiCall<T>(
  apiCall: () => Promise<T>
): Promise<Result<T>> {
  try {
    const data = await apiCall();
    return { success: true, data };
  } catch (error) {
    return {
      success: false,
      error:
        error instanceof Error
          ? error
          : new Error('Unknown error'),
    };
  }
}

// 使用例
async function getUserSafely(
  id: UserId
): Promise<Result<BrandedUser>> {
  return safeApiCall(() => fetchUser(id));
}

// エラーハンドリングも型安全
async function handleUser(id: string) {
  const userId = createUserId(id);
  const result = await getUserSafely(userId);

  if (result.success) {
    console.log('User:', result.data.name);
  } else {
    console.error('Error:', result.error.message);
  }
}

まとめ(型安全な SSR/SSG 開発のベストプラクティス)

本記事では、Next.js × TypeScript での SSR・SSG 開発における型定義のベストプラクティスを包括的に解説いたしました。適切な型定義により、開発体験とアプリケーションの信頼性を大幅に向上させることができます。

重要なポイントの再確認

1. 基本設定の重要性

  • tsconfig.json の適切な設定により、TypeScript の恩恵を最大限に活用
  • プロジェクト構造に応じたパス設定とモジュール解決の最適化
  • 段階的な厳密性の導入による無理のない移行

2. Pages Router vs App Router の使い分け

  • Pages Router: GetServerSidePropsGetStaticProps での型安全なデータフェッチング
  • App Router: Server Components での新しい型定義アプローチ
  • それぞれの特性を理解した適切な選択が重要

3. データフェッチング層の設計

  • API クライアントの型安全な実装によるエラー削減
  • リソース別の型定義による保守性向上
  • カスタムフックによる再利用可能なデータフェッチング

4. 動的ルーティングでの型安全性

  • URL パラメータとクエリパラメータの適切な型定義
  • バリデーションライブラリとの組み合わせによる実行時安全性
  • エラーハンドリングと型安全性の両立

5. API Routes での型定義

  • リクエスト・レスポンスの型安全性確保
  • ミドルウェアでの型安全な認証・認可
  • エラーレスポンスの統一と型定義

6. パフォーマンス最適化

  • TypeScript コンパイル時間の短縮
  • Bundle サイズの最適化
  • 段階的な型安全化による開発効率向上

実装時の注意点

型安全な Next.js アプリケーション開発では、以下の点に注意が必要です。

型定義の粒度 過度に複雑な型定義は、かえって開発効率を下げる可能性があります。プロジェクトの要件と開発チームのスキルレベルに応じて、適切な粒度で型定義を行うことが重要です。

パフォーマンスとのトレードオフ 型安全性とパフォーマンスはトレードオフの関係にある場合があります。特に TypeScript のコンパイル時間や複雑な型推論は、開発体験に影響を与える可能性があるため、バランスを考慮した実装が必要です。

チーム開発での統一 型定義のガイドラインを策定し、チーム全体で一貫した実装を行うことで、保守性と開発効率を向上させることができます。

今後の展望

Next.js と TypeScript のエコシステムは継続的に進化しており、以下のような機能強化が期待されます。

  • App Router のさらなる成熟と型安全性の向上
  • TypeScript 5.x の新機能との連携強化
  • Edge Runtime での型定義サポート拡充
  • 開発ツールとの統合による DX 向上

これらの進化に対応しながら、型安全で保守性の高い Next.js アプリケーションを開発していくことが、現代の Web 開発における重要なスキルとなるでしょう。

型定義は一度設定すれば終わりではなく、アプリケーションの成長とともに継続的に改善していく必要があります。本記事で紹介したベストプラクティスを参考に、プロジェクトの要件に最適な型安全性を実現していただければと思います。

関連リンク

公式ドキュメント

型定義関連ライブラリ

開発ツール

コミュニティリソース