JavaScript Temporal:用對型別處理日期與時區
介紹 JavaScript Temporal 的 Instant、ZonedDateTime、PlainDate 與日期運算,並分享在 Electron 專案 Solyx 的實作與支援注意事項。
- 筆記
- 7 分鐘閱讀
以前在 JavaScript 專案處理日期,我通常會先裝 dayjs。它的 API 比原生 Date 順手,格式化和日期加減都內建,時區轉換也能透過 plugin 補上。最近我整理自己的 Electron app Solyx,一開始也想把散落的 Date 和 Intl 統一改成 dayjs;盤點後發現,真正需要處理的是少數時區與日曆運算。

Solyx 用來交易台股與美股,agent 會分析並提出交易建議,每筆下單都需要使用者確認。我最後沒有換成 dayjs,而是用 Temporal 改寫日期運算,顯示格式則保留 Intl。原因是 dayjs 的 timezone plugin 底層仍需透過 Intl.DateTimeFormat#formatToParts 推算時區偏移,並沒有省去這部分邏輯;locale 也要另外載入與維護,plugin 還需要透過全域的 dayjs.extend(...) 註冊。套件間原本傳遞的是 Date、Unix 秒和日期字串,若改傳 dayjs 物件,還得調整介面或在邊界反覆轉換。
Temporal 是 JavaScript 新的日期時間 API,將時間點、帶時區的日期時間和不帶時區的日曆日期分開表示。以下整理我在 Solyx 實際用到的幾個型別。
先分清楚時間代表什麼
Temporal 不再用一種日期物件承擔所有語意,常用型別包括:
| 型別 | 表示的資料 | 常見用途 |
|---|---|---|
Temporal.Instant | 時間軸上的確切時間點 | Unix timestamp、事件發生時間 |
Temporal.ZonedDateTime | 日期時間加上時區 | 顯示或計算某個時區的當地時間 |
Temporal.PlainDate | 不帶時間與時區的日曆日期 | 生日、交易日、帳單日期 |
Temporal.PlainTime | 不帶日期與時區的時間 | 每日開盤時間、提醒時間 |
Temporal.PlainDateTime | 不帶時區的日期與時間 | 尚未指定時區的當地時間 |
Temporal.Duration | 一段時間或日期差值 | 表示加減或兩個時間之間的差距 |
例如手上有一個 Date,可以先轉成 Instant,再依交易所時區取得當地日期時間:
const instant = Temporal.Instant.fromEpochMilliseconds(date.getTime());
const exchangeTime = instant.toZonedDateTimeISO("America/New_York");
exchangeTime.toPlainDate().toString(); // 例如 "2026-10-06"這裡的 Instant 是同一個確切時間點;ZonedDateTime 則用時區規則呈現它在紐約當地的日期與時間。若只需要日曆日期,就取出 PlainDate,不必把「某天」當成 UTC 午夜來處理。
日期加減直接使用日曆語意
PlainDate 支援日曆日期運算。加一天、找該月第一天,或取得該週星期一,不需要自行換算毫秒:
const day = Temporal.PlainDate.from("2026-01-31");
day.add({ days: 1 }).toString(); // "2026-02-01"
day.add({ months: 1 }).toString(); // "2026-02-28"
day.with({ day: 1 }).toString(); // 該月第一天 "2026-01-01"
day.subtract({ days: day.dayOfWeek - 1 }).toString(); // 該週的星期一 "2026-01-26"dayOfWeek 使用 ISO 星期編號:週一是 1,週日是 7。月份加減若遇到目標月份沒有相同日期,預設會夾到月底,所以 1 月 31 日加一個月是 2 月 28 日。
Solyx 的圖表需要找出某一天所在週的週一、或當月第一天;交易時段也會依交易所時區判斷星期幾。這些都是 PlainDate 和 ZonedDateTime 比手動拆字串或呼叫 getUTCDay() 更直接的地方。
取得某個時區的當地午夜
若有 YYYY-MM-DD 日曆日期,要取得它在指定時區的一天起點,可以將 PlainDate 轉成 ZonedDateTime:
const startOfDay = Temporal.PlainDate
.from("2026-10-06")
.toZonedDateTime("America/New_York");
const unixSeconds = startOfDay.epochMilliseconds / 1000;這比先假設日期是 UTC 午夜、再用 offset 推回當地時間來得直接。時區規則包含日光節約時間等變化;某些地區切換時間時,當地午夜可能不存在或重複,因此不要假設每個日曆日都剛好是 24 小時。
在 Solyx 的舊版實作裡,我曾用 UTC 午夜的 offset 推算交易所當地午夜,這依賴「交易所不會在午夜附近調時」的假設。改用 Temporal 後,程式直接以日期和時區建立當地起點,不必自行估算 offset。
計算與顯示分開
Temporal 適合處理日期時間的表示與計算;要把結果呈現給使用者,仍可交給 Intl,讓格式依 locale 決定:
const formatter = new Intl.DateTimeFormat("zh-TW", {
timeZone: "America/New_York",
dateStyle: "medium",
timeStyle: "short",
});
formatter.format(new Date());要注意 Intl.DateTimeFormat#format 不接受 ZonedDateTime,直接傳進去會丟 TypeError。要格式化 Temporal 算出的結果,例如前面的 exchangeTime,可以呼叫它自己的 toLocaleString:
exchangeTime.toLocaleString("zh-TW", { dateStyle: "medium", timeStyle: "short" });同一組格式選項會重複使用時,建立並重用 Intl.DateTimeFormat 實例即可,不必每次都重新建立 formatter。相對時間和數字也可以使用 Intl.RelativeTimeFormat、Intl.NumberFormat 等 API。
使用前先確認 runtime
有 TypeScript 型別不代表 runtime 一定支援 Temporal。Node.js 從 26.0.0 起預設啟用 Temporal(Node.js 26 release notes),使用前仍要確認目標 runtime 的支援情況。
TypeScript 也需要適當的 library 宣告:我使用的 TypeScript 7.0.2 在 lib: ["es2023"] 下找不到 Temporal,加入 "esnext.temporal" 後才通過型別檢查。型別設定只影響編譯器認不認得 API,不會替舊 runtime 補上實作。

截至 2026 年 10 月,MDN 相容性表列出 Chrome/Edge 144、Firefox 139 與 Node.js 26 已支援 Temporal;Safari 目前只有 Technology Preview 支援,iOS Safari 與 WKWebView 尚不支援(MDN:Temporal)。Electron 或 Node 等 runtime 可控的專案,可以先確認 app 和測試實際使用的版本;公開網站則需考慮 polyfill 或功能偵測。
我目前採用的簡單分工是:Date 留在序列化、DB/API 資料和時鐘等邊界;Temporal 負責時區與日曆計算;Intl 負責在地化顯示。
Written by: Chia1104 CC BY-NC-SA 4.0