TL

Temporal APIと日時計算の正確なモデル

Dateのバグに振り回される日々から抜け出せる。可変・タイムゾーン非対応というDateの構造的欠陥と、不変オブジェクト・暦/タイムゾーン明示・PlainとZonedの分離で正しさを設計するTemporalの考え方を原理から整理します。

応用JavaScriptTemporal日時タイムゾーン夏時間Date最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.JavaScriptのDateは可変・月が0始まり・パースが実装依存・内部はUTCミリ秒の1値のみという設計上の欠陥を抱え、ローカル時刻とUTCの境界でバグを生みやすい。
  • 2.Temporalは全オブジェクトが不変(immutable)で、タイムゾーンを持たないPlain系(壁掛け時計の時刻)と、特定地点の物理的瞬間を表すZonedDateTimeを型として分離する。
  • 3.夏時間の切替やうるう秒のような不規則さは、PlainとZonedの変換時に暦・タイムゾーン規則を明示的に適用して解決する。Instantは連続したエポック秒で、うるう秒を持たない。

なぜ日時APIに「正確なモデル」が要るのか

日時のバグは「たまにずれる」形で表面化するため、再現も発見も難しいのが厄介です。月初の集計が1日ずれる、夏時間の切替日だけ予約が二重になる、サーバーとブラウザで同じ「3月1日」が別の瞬間を指す——これらは個別のコーディングミスというより、時刻を1つの数値で雑に表すという旧来モデルの帰結です。Temporalは「壁掛け時計の時刻」と「物理的な瞬間」を別物として型で区別し、暦やタイムゾーンの規則を明示的に適用させることで、これらを構造的に防ぎます。前提として基礎は JavaScript を、時刻が内部で数値として持たれる点は JavaScriptの数値表現とIEEE754が生む落とし穴 を押さえておくと理解が早いです。

Date の設計上の欠陥

Date は1995年のJavaScript誕生時にJavaの旧 java.util.Date を写して作られ、その欠陥ごと引き継ぎました。中心的な問題は次の通りです。

欠陥具体例なぜ問題か
可変(mutable)d.setMonth(5) が元オブジェクトを破壊共有された Date を渡すと予期せず書き換わる
月が0始まりnew Date(2026, 0, 1) が1月1日日・年は1始まりなのに月だけ0始まりで混乱
パースが実装依存new Date('2026-03-01') の解釈がブラウザ間で割れるISO以外の文字列で結果が再現しない
内部はUTCミリ秒1値のみタイムゾーンを保持できないローカル時刻とUTCの境界で必ず変換が要る

Date の実体はエポック(1970-01-01T00:00:00 UTC)からの経過ミリ秒という1つの数値だけです。タイムゾーン情報を内部に持たず、getHours() のような「ローカル系」のメソッドは呼び出した実行環境のタイムゾーンを使って毎回その場で換算します。つまり同じ Date 値でも、東京のサーバーとロンドンのブラウザでは getDate() が別の日を返し得ます。

可変であることが最大の地雷

Date には「N日後」を新しい値として返すメソッドがなく、setDate(d.getDate() + 7) のように自身を破壊して進めるしかありません。引数で受け取った Date をうっかり加工すると呼び出し元の値まで変わります。等価判定も d1 === d2 は参照比較で常に falsed1 == d2 も期待通り動かないため d1.getTime() === d2.getTime() を強いられます。この型変換の罠は 型変換と等価比較の落とし穴 と同根です。

Temporal の中核:不変オブジェクトと型の分離

Temporalはこれらを根本から作り直した提案です。設計原則は2つに集約できます。

  1. 全オブジェクトが不変(immutable)add with round などはすべて元を変えず新しいインスタンスを返す。共有しても安全で、加工の副作用が消えます。
  2. 「時刻の種類」を型で分ける:タイムゾーンを持たない「壁掛け時計の時刻」(Plain系)と、地球上の特定の瞬間(Instant / ZonedDateTime)を別の型として区別します。

この型分離が本質です。たとえば「毎朝9時に通知」の9時は、どのタイムゾーンでも9時であってほしい壁掛け時計の時刻Plain です。一方「打ち上げは協定世界時の正確なこの瞬間」は地球上で一意な物理的瞬間Instant です。両者を1つの Date で兼ねていたことが、旧来の混乱の元でした。

持つ情報代表的な用途
Temporal.Instantエポックからの連続した時刻(ナノ秒精度)ログのタイムスタンプ、一意な瞬間の記録
Temporal.PlainDate年月日のみ(暦付き、TZなし)誕生日、請求の締め日
Temporal.PlainTime時分秒のみ(日付・TZなし)営業開始時刻 09:00
Temporal.PlainDateTime年月日+時分秒(TZなし)TZ未確定の予定の素材
Temporal.ZonedDateTime瞬間+タイムゾーン+暦の全部入り会議予約、地域に紐づく日時
// すべて不変。元のインスタンスは変わらず、新しい値が返る
const d = Temporal.PlainDate.from('2026-03-01'); // 月は1始まり
const next = d.add({ days: 7 });                 // 2026-03-08(dはそのまま)

// 物理的な瞬間。タイムゾーンに紐づけて初めて「壁掛け時刻」になる
const meeting = Temporal.ZonedDateTime.from(
  '2026-03-01T09:00[Asia/Tokyo]'
);
meeting.toInstant().epochNanoseconds; // 全世界で一意な瞬間

PlainDateTime は「3月1日9時」までは決めるがタイムゾーンを欠くため、まだ物理的瞬間ではありません。これに Asia/Tokyo のようなタイムゾーンを与えて初めて ZonedDateTime(=一意な Instant)が確定します。Plain と Zoned の往復で必ずタイムゾーンを通すこの設計が、後述の夏時間問題を表に引きずり出します。

Plain と Zoned の境界:夏時間(DST)をどう扱うか

夏時間(DST)の切替日には、存在しない時刻二度ある時刻が生まれます。たとえば米国で春に時計が午前2時から3時へ飛ぶ日、02:30実在しません。秋に3時から2時へ戻る日、01:301日に2回訪れます。Date はこの不規則さを暗黙に握りつぶし、どちらの瞬間を指すか不定でした。

Temporalは PlainDateTime(壁掛け時刻)から ZonedDateTime(瞬間)へ変換する瞬間に、この曖昧さを明示的な選択肢として扱わせます。

disambiguation存在しない時刻二度ある時刻
'compatible'(既定)後ろ側(gap後)の瞬間を採用前側(最初の出現)を採用
'earlier'gap直前の瞬間最初の出現
'later'gap直後の瞬間2回目の出現
'reject'RangeError を投げるRangeError を投げる
// DSTで存在しない壁掛け時刻を瞬間へ写すとき、挙動を選べる
const pdt = Temporal.PlainDateTime.from('2026-03-08T02:30');
pdt.toZonedDateTime('America/New_York', { disambiguation: 'reject' });
// → RangeError(存在しない時刻を握りつぶさず明示的に弾く)

重要なのは、ZonedDateTime 同士の「N日後」と「N時間後」が別物になる点です。add({ hours: 24 }) は物理的に24時間後の瞬間へ進めるのに対し、add({ days: 1 }) は暦の上で翌日の同じ壁掛け時刻へ進めます。DST切替を跨ぐ日、後者は実際の経過が23時間や25時間になり得ます。Date ではこの区別が表現できず、「日付の加算」を時間ミリ秒で代用したコードが切替日だけ1時間ずれていました。

タイムゾーン規則はIANA TZ DBが正

夏時間の開始・終了日や各地のオフセットは法律で頻繁に変わるため、ハードコードしてはいけません。Temporalは America/New_York のようなIANAタイムゾーン識別子を使い、実行環境が持つIANA Time Zone Databaseの規則を参照します。+09:00 のような固定オフセットも指定できますが、それはDSTの切替規則を持たない「ただのずれ」であって、地域の時刻ルールとは別物です。識別子(地域名)とオフセット(数値のずれ)を混同しないことが頻出ポイントです。

暦(カレンダー)とうるう秒の正しい位置づけ

ZonedDateTimePlainDate は**暦(calendar)**も保持します。既定は iso8601(グレゴリオ暦の代理)ですが、japanese(和暦)や hebrewislamic なども扱え、year month のような数値が暦ごとに正しく解釈されます。たとえば年内の月数が12でない暦では、add({ months: 1 }) の意味が暦規則に従って決まります。これも「数値の足し算」では表せなかった情報です。

うるう秒の扱いは特に誤解されがちです。Temporalの Instant と内部時刻はうるう秒を持たない連続したタイムライン(UTCのうるう秒を平滑化したPOSIX的なエポック時刻)で表されます。つまり 23:59:60 のような値は受け付けず、60 秒は存在しないものとして丸められます。

なぜうるう秒を持たないのか

うるう秒は地球の自転のゆらぎに合わせて不定期に挿入される1秒で、未来の挿入予定を事前に確定できません。これをそのまま時刻計算に持ち込むと、過去の任意の2瞬間の差さえ将来の挿入で変わりかねず、計算が不安定になります。そこでTemporalは多くのOSやデータベースと同様、うるう秒を平滑化した連続軸を採用し、厳密な物理時刻が要る用途(天文・測位など)は専用系に委ねる、という割り切りをしています。連続したエポック値という考え方は JavaScriptの数値表現とIEEE754が生む落とし穴 の整数精度とも関わり、Instant は内部でナノ秒精度を扱うため BigIntepochNanoseconds を公開します。

永続化と相互運用

不変で型が分かれている分、保存・送信時の形式選びも明確になります。物理的な瞬間を保存するなら、タイムゾーンまで含む ZonedDateTime のISO文字列(...[Asia/Tokyo] 付き)か、地点情報が不要なら Instant(UTCの Z 付き)を使います。一方、壁掛け時刻そのもの(毎朝9時など)はタイムゾーンを付けずに PlainTime / PlainDate として保存するのが正解で、ここに無理にタイムゾーンを与えると「どこかの9時」に固定されて意図がずれます。

// 瞬間として保存(地点も復元したいなら ZonedDateTime 文字列)
zdt.toString();        // "2026-03-01T09:00:00+09:00[Asia/Tokyo]"
zdt.toInstant().toString(); // "2026-03-01T00:00:00Z"(UTCの瞬間のみ)

Temporalオブジェクトは構造化クローンの対象にもなり得ますが、ワーカー間やストレージへ渡す際は文字列化してから扱うのが堅実です(クローンの可否や落とし穴は 構造化クローンとTransferable を参照)。Date との橋渡しは Temporal.Instant.fromEpochMilliseconds(date.getTime()) などで瞬間を介して行い、ローカル時刻のメソッドを経由しないのが安全です。

まとめ

まとめ

Date可変・月0始まり・パース実装依存・内部はUTCミリ秒の1値という構造的欠陥を抱え、壁掛け時刻と物理的瞬間を1つの数値に押し込めたことが日時バグの温床でした。Temporalはこれを全オブジェクト不変型の分離で作り直し、タイムゾーンを持たない Plain 系と、特定の瞬間を表す Instant / ZonedDateTime を別物として扱います。両者の変換時にタイムゾーンを必ず通すため、夏時間の存在しない時刻・二度ある時刻disambiguation の明示的な選択として表面化し、暗黙の握りつぶしが消えます。タイムゾーン規則はIANA TZ DBが正、暦は明示でき、Instant はうるう秒を持たない連続軸です。「その時刻は壁掛けか、瞬間か」を最初に決める習慣が、再現しにくい日時バグを設計段階で断ちます。

Web/フロントエンド Article

Temporal APIと日時計算の正確なモデルを実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

JavaScript

比較で見る軸

難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 6

導入後に効く点

Temporalは全オブジェクトが不変(immutable)で、タイムゾーンを持たないPlain系(壁掛け時計の時刻)と、特定地点の物理的瞬間を表すZonedDateTimeを型として分離する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
Web/フロントエンド
タグ数
6

判断チェックリスト

  • 自社の用途が「JavaScript / Temporal」に近いか確認する。
  • 強みである「JavaScriptのDateは可変・月が0始まり・パースが実装依存・内部はUTCミリ秒の1値のみという設計上の欠陥を抱え、ローカル時刻とUTCの境界でバグを生みやすい。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

JavaScriptTemporal日時タイムゾーン夏時間JavaScriptTemporal日時
参考: 公式情報