Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Pages: loader props lose `Date` (and other non-JSON types) and their types don't say so

オープン
#606 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
35/100
issue の種類
機能追加
明瞭さ
おおむね明確
活発さ
活発
技術スタック
typescript
領域
full-stack

調査の方向性

Start by reading pages/client.mjs and the server serialization entry points named in the issue: serializePageData, streamDeferredResponse, and the page-object responses. Trace how initial page data, deferred chunks, navigation, and action responses are serialized and parsed, then compare the behavior with the existing Serialize<T> fetch-client types. Done means the selected transport and InferProps types agree for supported values, with coverage for server rendering, hydration, and later navigations.

索引モデルが issue の本文から書いたものです。

説明

Pages: loader props lose Date (and other non-JSON types) and their types don't say so

What happens

Page data is sent as plain JSON, but InferProps types the props as the loader's raw return type. A loader that returns a Drizzle row with a timestamp_ms column (every Void auth table has these) gives the page a prop typed Date, which is only a Date during the server render:

Where createdAt is
InferProps<typeof loader> Date
Server render Date (raw loader output)
Browser, hydration and later navigations/actions string (ISO, from JSON.parse)

Code that type-checks then crashes in the browser. For example, Intl.DateTimeFormat#format(note.createdAt) renders on the server, then throws RangeError: Invalid time value during hydration.

// pages/notes.server.ts
export type Props = InferProps<typeof loader>;
export const loader = defineHandler(async () => ({
  notes: await db.select().from(noteTable), // createdAt: Date
}));

// pages/notes.tsx
const fmt = new Intl.DateTimeFormat('en', { dateStyle: 'medium' });

export default function Notes({ notes }: Props) {
  return notes.map((n) => <p key={n.id}>{fmt.format(n.createdAt)}</p>);
  // server: renders. browser: RangeError: Invalid time value
}

The same happens with Map/Set (they become {}), undefined fields (dropped), and bigint (throws).

Today the workaround is to convert every timestamp at the loader (createdAt: row.createdAt.getTime()).

The typed fetch client avoids the type problem by wrapping responses in Serialize<T> (Date → string), as described in the type-safety guide. Page props, which travel through the same JSON, don't get that treatment. Every meta-framework Void integrates with keeps Date in page data: SvelteKit and Nuxt (devalue), React Router (turbo-stream), TanStack Start (seroval). So this only affects native Void pages.

Proposal: serialize page data with devalue

devalue.stringify / devalue.parse preserve Date, Map, Set, bigint, undefined, RegExp, URL and repeated or cyclic references. The output is still JSON text, so it fits the existing <script type="application/json"> payload and escapeScriptJson. parse never evaluates code. SvelteKit and Nuxt use it for exactly this job.

The JSON calls that would need to switch (from the built [email protected] dist):

  • Server:
    • serializePageData and the frontmatter script (serialize-*.mjs)
    • the page-object and deferred-chunk JSON.stringify calls in streamDeferredResponse (protocol-*.mjs)
    • the JSON page-object response for client navigations and actions
  • Client (pages/client.mjs):
    • readInitialPageData
    • parsePageDataFromHtml
    • the navigation and action response.json() reads
    • clonePageData (could become structuredClone, which also keeps Date)

After the change:

  • The server render and the browser see the same values.
  • InferProps stays the raw return type, which is then accurate for everything devalue supports.
  • Loaders can return Drizzle rows directly.

Values devalue can't handle (functions, class instances) could be stripped from InferProps with a small mapped type, the way Serialize<T> already strips functions.

Alternatives
  • Minimal fix, no format change: apply Serialize<T> to InferProps, and render the server page from JSON.parse(JSON.stringify(props)). Types and runtime would then agree, but dates stay strings.
  • seroval: covers more types and can also stream Promises, so it could replace the custom defer() streaming. It's a bigger change, and some of its output modes are JavaScript that the browser evaluates.
Related
  • #478: InferProps reads __output directly, while routes wrap it in Serialize<…>.
  • #443: RSC for pages would change the transport for the React adapter. Page data is JSON (NDJSON for deferred props) today.
主要言語
TypeScript
スター
580
フォーク
56
平均マージ
11時間 28分
マージ済み PR(30日)
150

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

voidzero-dev/void のほかの issue

voidzero-dev/void の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。