理系大学院生のぼちぼちITノート

Next.js App Router環境下で、Vercelビルド時に発生するparamsの型エラーとその解決策

発生状況と技術環境

Next.jsのApp Routerを採用したプロジェクトにおいて、ローカル開発環境では問題が顕在化しないものの、Vercelへのデプロイ時に実行されるnext buildの過程でビルドが失敗するという状況に直面しました。

開発環境は以下の通りです。

  • フレームワーク: Next.js 15.2.3 (App Router)
  • 言語: TypeScript
  • CMS: microCMS
  • デプロイ環境: Vercel

エラーログの確認

Vercelのビルドログには、下記のエラーが出力されていました。
このエラーは、動的ルートを持つ複数のpage.tsxlayout.tsxで連鎖的に確認されました。

Type error: Types of property 'params' are incompatible.
  Type '{ categoryId: string; }' is missing the following properties from type 'Promise<any>': then, catch, finally, ...

原因の分析

このエラーの根本的な原因は、Next.js App RouterにおけるServer Componentの仕様にあります。

動的なルートセグメント(例: app/categories/[categoryId]//page.tsx])を持つpage.tsxlayout.tsxのようなServer Componentに対して、Next.jsはパフォーマンス最適化の一環として、paramsプロパティをPromiseオブジェクトとしてコンポーネントに渡します。

一方で、私のコードではparamsの型を通常のオブジェクトとして定義していました。
ローカルの開発サーバー(npm run dev)は型チェックが比較的緩やかであるためこの不整合を見過ごしますが、本番用のビルドコマンド(npm run build)はプロジェクト全体に対して厳格な型検査を実施するため、エラーとして検出されました。

コードの修正

この問題を解決するためには、paramsプロパティをPromiseとして受け取り、コンポーネント内でawaitを用いてその値を取り出すようにコードを修正します。

修正前のコード (app/categories/[categoryId]/page.tsxの例)
// 【問題点】paramsを単なるオブジェクトとして型定義している
type Props = {
  params: {
    categoryId: string;
  };
};

export default async function Page({ params }: Props) {
  // `params`はPromiseであるため、この時点ではプロパティに直接アクセスできない
  const { categoryId } = params;
  const data = await getList({ filters: `category[equals]${categoryId}` });
  // ...
}
修正後のコード (app/categories/[categoryId]/page.tsxの例)
// 【解決策】paramsをPromise<T>として正しく型定義する
type Props = {
  params: Promise<{
    categoryId: string;
  }>;
};

// `params`を別名(例: paramsPromise)で受け取り、awaitで解決する
export default async function Page({ params: paramsPromise }: Props) {
  const params = await paramsPromise;

  // 解決後のオブジェクトからプロパティにアクセスする
  const { categoryId } = params;
  const data = await getList({ filters: `category[equals]${categoryId}` });
  // ...
}

この修正を、エラーが報告された全ての動的ルートコンポーネントに適用することで、ビルドが成功に至りました。

paramsにおけるオブジェクト型とPromise型の違い

今回のエラーを理解する上で、両者の技術的な違いを明確にすることが重要です。

  • オブジェクト型 ({ categoryId: string }):これは同期的(Synchronous)な値です。この型の変数を参照する時、その中身(プロパティ)は既に確定しており、即座に利用できる状態にあることを示します。
  • Promise型 (Promise<{...}>):こちらは非同期(Asynchronous)な処理の結果を表現するためのオブジェクトです。値そのものではなく、「将来、値が確定した際にそれを受け取るための引換券」と考えることができます。実際の値にアクセスするためには、await式を用いるか、.then()メソッドを通じて、非同期処理の完了を待つ必要があります。