Next.js App RouterでDate型をpropsに渡せない問題と現実的な解決策

開発

Next.js App RouterでDate型をpropsに渡そうとしたら、こんなエラーが出たことありませんか?

Error: Only plain objects can be passed to Client Components from Server Components.
Date objects are not supported.

「Dateはシリアライズできるはずなのに、なぜ?」と思いますよね。この記事では、この問題の背景と、型安全性を保ちながら解決する方法を解説します。

なぜDateオブジェクトを渡せないのか

Server ComponentからClient Componentにpropsを渡すとき、ReactはJSON形式でデータをシリアライズします。

// Server Component
async function Page() {
  const post = await getPost();
  return <ArticleClient date={post.date} />; // ❌ エラー
}

// Client Component
'use client';
function ArticleClient({ date }: { date: Date }) {
  return <time>{date.toLocaleDateString()}</time>;
}

問題はJSON.stringifyJSON.parseの挙動です。

const date = new Date('2026-02-02');
const json = JSON.stringify({ date });
// → '{"date":"2026-02-02T00:00:00.000Z"}'

const parsed = JSON.parse(json);
// → { date: "2026-02-02T00:00:00.000Z" }  // 文字列のまま!

Dateは文字列に変換されますが、JSON.parseでは自動的にDateに戻らない。Vercelチームはこのハイドレーション不一致を防ぐため、意図的にDateを禁止しています。

Vercelチームの見解

GitHub Issue #13209でのJoe Haddad(Vercel)の説明:

“This check is necessary as we do not serialize props on the server for performance reasons.”

Tim NeutkensもDiscussion #11498で:

“Date is serialized to a string and then on the client-side would cause a hydration error”

パフォーマンス優先の設計判断であり、変更する予定はなさそうです。

string vs Date:設計原則の観点から

ここで「じゃあ全部stringで渡せばいいじゃん」と思うかもしれません。でも、ちょっと待ってください。

Matias Kinunenのブログでは、こう主張しています:

“A date string is not a date; it’s a serialized format of a date.”

つまり、日付文字列は日付ではなく、シリアライズされた形式だと。

Viewコンポーネントの責務

コンポーネント設計の観点から考えてみましょう。

// Viewの責務は「どう表示するか」
function FormattedDate({ date }: { date: Date }) {
  return <time>{format(date, 'yyyy/M/d')}</time>;
}

Viewコンポーネントは:

  • Dateを受け取る
  • 好きな形式にフォーマットする

これが自然な責務分離です。

// ❌ Viewがパースの責務を負っている
function FormattedDate({ date }: { date: string }) {
  const parsed = new Date(date); // パースはViewの仕事?
  return <time>{format(parsed, 'yyyy/M/d')}</time>;
}

stringを受け取ると:

  • Viewがパースの責務を負う(責務違反)
  • フォーマット形式の知識がViewに漏れる
  • 不正な文字列のエラーハンドリングもViewの責務になる

「Viewがパースするのはおかしい」という直感は正しいと思います。でもNext.jsの技術的制約がある。このジレンマが厄介なんですよね。

筆者

現実的な解決策

設計原則と技術的制約の両立を考えると、いくつかのパターンがあります。

パターン1: Server Componentで完結させる

日付表示にインタラクティビティが不要なら、これが最もシンプルです。

// Server Component内で完結
async function Page() {
  const post = await getPost();
  return (
    <article>
      <h1>{post.title}</h1>
      <FormattedDate date={post.date} />  {/* Server Component */}
      <LikeButton postId={post.id} />      {/* Client Component - 日付不要 */}
    </article>
  );
}

// FormattedDateもServer Component
function FormattedDate({ date }: { date: Date }) {
  return <time>{date.toLocaleDateString('ja-JP')}</time>;
}

Server Component → Server ComponentならDateをそのまま渡せます。

Next.js Server Componentでバケツリレーを解消する方法:データ取得パターン完全ガイド開発

パターン2: 境界で変換してViewはDateを受け取る

どうしてもClient Componentで日付を使う場合、境界層で変換します。

// Server Component
async function Page() {
  const post = await getPost();
  return (
    <InteractiveArticle
      {...post}
      date={post.date.toISOString()}  // 境界で文字列化
    />
  );
}

// Client Component(境界層)
'use client';
function InteractiveArticle({ date, ...rest }: { date: string }) {
  const dateObj = new Date(date);  // 即座にDateに変換

  return (
    <article>
      <FormattedDate date={dateObj} />  {/* Dateを渡す */}
    </article>
  );
}

// Viewは常にDateを受け取る
function FormattedDate({ date }: { date: Date }) {
  return <time>{date.toLocaleDateString('ja-JP')}</time>;
}

境界層がパースを担当し、View層は常にDateを受け取る。設計原則を守りつつ、技術的制約に対応できます。

パターン3: Branded Typeで型安全性を高める

plain stringだと、任意の文字列を渡せてしまいます。Branded Type(名目的型)を使えば、コンパイル時に変換忘れを防げます

// 型定義
type ISODateString = string & { readonly __brand: unique symbol };

// 作成関数(ここで検証もできる)
function toISODateString(date: Date): ISODateString {
  return date.toISOString() as ISODateString;
}

// 復元関数
function fromISODateString(iso: ISODateString): Date {
  return new Date(iso);
}

使い方:

// Server Component
async function Page() {
  const post = await getPost();
  return (
    <InteractiveArticle
      date={toISODateString(post.date)}  // 明示的に変換
    />
  );
}

// ❌ plain stringを渡すと型エラー
<InteractiveArticle date="2026-02-02" />  // Type 'string' is not assignable to type 'ISODateString'

// ✅ toISODateStringを通すと型が通る
<InteractiveArticle date={toISODateString(new Date())} />

パターン4: superjsonで透過的に変換

superjsonを使えば、DateMapSetなどを透過的にシリアライズできます。

npm install superjson next-superjson-plugin
// next.config.js
module.exports = {
  experimental: {
    swcPlugins: [["next-superjson-plugin", {}]],
  },
};

これでDateをそのまま渡せます…が、注意点があります。

superjsonは実データをjsonプロパティに、型情報をmetaプロパティに格納します。そのため、JSONサイズにオーバーヘッドが発生します。チャートデータなど大量の日付を扱う場合は要注意です。

また、SWC Pluginは実験的機能です。本番環境で使う場合はリスクを理解しておきましょう。

型定義のパターン

状態の組み合わせを型で制限する|React props設計開発

実際のプロジェクトで使える型定義を整理します。

基本パターン: Branded Type

// types/date.ts
declare const brand: unique symbol;
export type ISODateString = string & { [brand]: 'ISODateString' };

export function toISODateString(date: Date): ISODateString {
  return date.toISOString() as ISODateString;
}

export function fromISODateString(iso: ISODateString): Date {
  const date = new Date(iso);
  if (isNaN(date.getTime())) {
    throw new Error(`Invalid ISO date string: ${iso}`);
  }
  return date;
}

DTOパターン: Server/Client境界用の型分離

// types/article.ts
export interface Article {
  id: string;
  title: string;
  date: Date;
}

// Server → Client 境界用
export interface ArticleSerialized {
  id: string;
  title: string;
  date: ISODateString;
}

// 変換関数
export function serializeArticle(article: Article): ArticleSerialized {
  return {
    ...article,
    date: toISODateString(article.date),
  };
}

export function deserializeArticle(article: ArticleSerialized): Article {
  return {
    ...article,
    date: fromISODateString(article.date),
  };
}

正直、DTO作るのは面倒です。でも型安全性と保守性を考えると、中〜大規模プロジェクトでは投資価値があると思います。

筆者

どのパターンを選ぶべきか

パターンメリットデメリット適するケース
SC完結最もシンプルCCで日付使えない日付表示がstatic
境界で変換責務が明確変換コードが必要一般的なケース
Branded Type型安全設定が必要型を重視するチーム
superjson透過的バンドル肥大化少数の日付のみ

個人的には、**「Server Componentで完結」を基本にして、CCが必要な場合は「境界で変換 + Branded Type」**がバランス良いと思います。

コミュニティの声

この問題について、ネット上では様々な意見があります。

設計原則派

“日付文字列は日付ではない。パース時にDateオブジェクトに変換するのが意味論的に正しい” - mtsknn.fi

現実派

“YYYY-MM-DD形式の文字列は比較・ソートが直感的に動く。JSONで往復するならstringで統一した方が事故が少ない” - Atomic Object

不満派

“Dateは実際にはJSONでシリアライズ可能なのに、なぜ制限するのか” - GitHub Issue #13209のコメント

コミュニティには様々な意見があります。「Dateが正しい」という設計原則は広く認められていますが、Next.js/RSCの制約から「stringで妥協」が現実解になっています。

まとめ

Next.js App RouterでDateをpropsに渡せない問題は、技術的制約と設計原則のトレードオフです。

  • Vercelチームの判断: パフォーマンスとハイドレーション一致のため、意図的にDateを禁止
  • 設計原則: Viewはパースの責務を負うべきではない、Dateを受け取るべき
  • 現実解: Server Componentで完結させるか、境界で変換してViewはDateを受け取る

型安全性を重視するなら、Branded Typeの導入をお勧めします。完璧な解決策ではありませんが、少なくともコンパイル時に変換忘れを防げます。

Thanks for reading!
Next.jsTypeScriptReactServer Components型安全性