JavaScript Temporal 可以直接用了嗎?新舊專案的導入判斷
驗證於
本文目錄
短答案:後端可以直接用,前端還要再等一下,但新程式碼已經可以照 Temporal 的寫法來寫。
Temporal 是取代 Date 的新日期時間 API,2026 年陸續在主要執行環境預設開啟。卡住前端的是 Safari:截至 2026-11-14,正式版 Safari 27.2 還沒有 Temporal,Safari 27.3 beta 的 release notes 才列出支援。正式版推出後,還要等 iPhone 使用者實際更新。
| 環境 | 狀態 | 我的做法 |
|---|---|---|
| Node.js 26 以上 | 26.0.0 起預設開啟,26 已是 LTS | 直接使用 |
| Node.js 24 以下 | 沒有內建 | 升級到 26,或搭配 polyfill |
| Chrome | Chrome 144 起支援 | 可用 |
| Firefox | Firefox 139 起支援 | 可用 |
| Safari/iOS | 27.3 beta 加入,正式版尚未推出 | 前端專案先搭配 polyfill |
前端要搭配 polyfill 時,我會選 temporal-polyfill。它的 temporal-polyfill/global 入口在瀏覽器已有原生 Temporal 時會直接使用原生版本,等 Safari 支援普及後,拿掉一行 import 就能收尾。
Date 會出錯、Temporal 寫得清楚的四個地方
台灣沒有夏令時間,很多日期 bug 平常碰不到,直到使用者或伺服器在別的時區才冒出來。下面這支腳本把四個典型問題寫成斷言:
import assert from 'node:assert/strict';
// 1. 只有日期的欄位:Date 把 '2026-03-08' 當成 UTC 午夜const legacy = new Date('2026-03-08');assert.equal(legacy.getDate(), 7); // 在洛杉磯時區讀到前一天const due = Temporal.PlainDate.from('2026-03-08');assert.equal(due.day, 8);
// 2. 1 月 31 日加一個月const d = new Date(2026, 0, 31);d.setMonth(d.getMonth() + 1);assert.equal(d.toDateString(), 'Tue Mar 03 2026'); // 溢出到 3 月const jan31 = Temporal.PlainDate.from('2026-01-31');assert.equal(jan31.add({ months: 1 }).toString(), '2026-02-28');assert.throws(() => jan31.add({ months: 1 }, { overflow: 'reject' }), RangeError);
// 3. 跨夏令時間的「明天同一時間」const meeting = Temporal.ZonedDateTime.from('2026-03-07T09:00[America/New_York]');const nextDay = meeting.add({ days: 1 });assert.equal(nextDay.toString(), '2026-03-08T09:00:00-04:00[America/New_York]');assert.equal(meeting.until(nextDay).total({ unit: 'hours' }), 23);assert.equal( meeting.add({ hours: 24 }).toString(), '2026-03-08T10:00:00-04:00[America/New_York]',);
// 4. 不存在的日期與不可變物件assert.throws(() => Temporal.PlainDate.from('2026-02-30'), RangeError);const start = Temporal.PlainDate.from('2026-11-14');const end = start.add({ days: 7 });assert.equal(start.toString(), '2026-11-14');assert.equal(end.toString(), '2026-11-21');
// 顯示成台北時間const instant = Temporal.Instant.from('2026-11-14T04:30:00Z');assert.equal( instant.toZonedDateTimeISO('Asia/Taipei').toPlainDateTime().toString(), '2026-11-14T12:30:00',);
console.log('Temporal 檢查通過', process.version);存成 temporal-check.mjs,用 Node.js 26 執行 TZ=America/Los_Angeles node temporal-check.mjs。TZ 只影響第一段 Date 的結果,用來重現海外使用者看到的狀況。
這四段裡,我認為最有價值的是第一段。生日、到期日、發票日期這類「只有日期」的欄位,用 Date 存的其實是某個時區的午夜時刻,換個時區就可能顯示成前一天。Temporal.PlainDate 從型別上就沒有時區,不會因為換了時區而位移。
第三段則說明了 Temporal 把「加一天」和「加 24 小時」分成兩件事。2026 年 3 月 8 日紐約進入夏令時間,那一天只有 23 小時:排程「每天早上九點」要用 days,計算「24 小時後到期」要用 hours。Date 沒有這個區分,結果取決於程式碼剛好怎麼寫。
月底加一個月的情況,Temporal 預設會停在 2 月 28 日;業務上不允許自動調整時,傳入 overflow: 'reject' 讓它拋錯。至少這個選擇變成明確的參數,而不是 setMonth() 默默溢出到 3 月。
新專案:照 Temporal 寫,前端加 polyfill
新專案我會直接以 Temporal 的型別來設計資料:只有日期的欄位用 PlainDate,真實發生的時間點用 Instant,需要依某地時間排程的用 ZonedDateTime。這個分類本身就能避免很多資料設計上的混淆,就算前端暫時要多載入 polyfill,也比先引入另一套日期函式庫、幾年後再搬一次划算。
後端如果跑在 Node.js 26,就不用任何額外套件。部署環境還停在 Node.js 24 的話,上面那支腳本在檔案開頭加一行 import 'temporal-polyfill/global'; 後,在 Node.js 24.13.0 也能通過同樣的斷言。
既有專案:從最容易出錯的地方開始換
已經用了日期函式庫、而且運作正常的專案,我不建議為了 Temporal 全面改寫。日期邏輯散在各處,全部替換的風險和測試成本都高,換來的多半只是寫法改變。
比較划算的是挑出最常出問題的部分先換:跨時區的排程、只有日期的欄位、和金額或期限有關的日期計算。這些地方改用 Temporal 之後,型別本身就是規格,未來的人比較不容易寫錯。其餘只是格式化顯示的程式碼,可以等到原本的函式庫需要升級時再一併處理。
格式化顯示的部分,Temporal 物件的 toLocaleString() 會使用 Intl.DateTimeFormat,處理方式可參考 Intl.DateTimeFormat 的時區與日期鍵。
上面的腳本在 2026-11-14 於 macOS 以 Node.js 26.13.1 執行通過;另在 Node.js 24.13.0 搭配 temporal-polyfill 1.0.8 執行同一份斷言,也全部通過。瀏覽器端的支援狀態依各瀏覽器官方 release notes 整理,沒有在 Safari 27.3 beta 上實測。
常見問題
Q: TypeScript 認得 Temporal 嗎?
A: TypeScript 6.0 起內建 Temporal 的型別。在 tsconfig.json 把 target 設成 esnext,或在 lib 加入 esnext,也可以只加更細的 esnext.temporal。型別檢查通過只代表編譯器認得這些 API,執行環境是否真的有 Temporal 要另外確認,前端仍需依上面的表格決定是否載入 polyfill。
Q: Temporal 物件可以直接放進 JSON 或資料庫嗎?
A: JSON.stringify() 會呼叫 Temporal 物件的 toJSON(),例如 PlainDate 會變成 "2026-03-08" 這樣的字串。讀回來時 JSON 不會記得它原本的型別,需要由程式自己用 Temporal.PlainDate.from() 還原。所以 API 和資料表的欄位最好事先約定型別:只有日期就存日期字串,時間點就存帶時區偏移的 ISO 字串或 epoch 數值,讀取端照約定還原。
參考資料: