ハクソク

世界を動かす技術を、日本語で。

日付は過去、時間は未来へ

概要

  • JavaScriptのDateオブジェクトは多くの混乱と非直感的な挙動を持つ
  • Dateの設計ミスにより、日付・時刻処理が一貫性を欠く
  • 不変性の原則に反し、Dateはミュータブルなオブジェクト
  • Temporal APIの登場で、Dateの問題が解消される見込み
  • 今後はTemporalの利用が推奨される流れ

JavaScriptのDateはなぜ嫌われるのか

  • Dateコンストラクタは月のみ0始まり、年や日は1始まりという直感に反する仕様
  • 文字列パースの挙動が一貫しない
    • 例: "2026-01-02""2026/01/02"で異なる日付解釈
  • 西暦の扱いが曖昧
    • 49 → 2049年、99 → 1999年、100 → 0100年など
  • 内部的にはUNIXタイムスタンプ(ミリ秒単位)で管理
  • タイムゾーンや夏時間のサポートが貧弱
  • グレゴリオ暦のみ対応、他の暦には非対応
  • JavaScriptのDateはJavaのDateを急いでコピーした産物であり、設計思想の欠如

Dateの本質的な問題点

  • JavaScriptのプリミティブ値は不変(immutable)
    • 例: 3trueは決して他の値にならない
  • Dateはオブジェクト=ミュータブル(mutable)
    • 例: new Date()で生成したオブジェクトは内部状態を変更可能
  • 参照渡しのため、意図せず値が変わるリスク
    • 例: 関数の引数でDateを渡すと、元の値も変化
  • 「1月1日」という絶対的な日付を不変値で表現できない矛盾
  • 現実世界の「日付」概念とプログラム上の実装の乖離

Dateのミュータブル性によるバグ例

  • 関数でDateを受け取り、日付を加算すると元のDateも書き換わる
    • 例:
      const today = new Date();
      const addDay = theDate => {
        theDate.setDate(theDate.getDate() + 1);
        return theDate;
      };
      console.log(`Tomorrow: ${addDay(today).toLocaleDateString()}`); // 1/1/2026
      console.log(`Today: ${today.toLocaleDateString()}`); // 1/1/2026(本来は12/31/2025であってほしい)
      
  • 参照のコピー=同じオブジェクトを指すため、予期せぬ副作用が発生

Dateの終焉とTemporalの登場

  • Dateは今後非推奨(deprecate)予定
    • ただし、完全な削除は難しいため「使わないことが推奨」される状態に
  • Temporal APIがDateの後継として標準化
  • Temporalはコンストラクタではなく、名前空間オブジェクト
    • 例: Temporal.NowTemporal.PlainDateなどの静的メソッド群
  • Mathオブジェクトと同様の使い勝手
  • より直感的・一貫性のある日付・時刻操作が可能

Temporalの特徴と今後の展望

  • 不変性(immutable)を重視した設計
  • 多様なカレンダー体系やタイムゾーンに対応
  • 副作用のない日付・時刻演算が可能
  • サードパーティ製ライブラリ不要な時代へ移行
  • 既存コードの移行には注意が必要だが、新規開発ではTemporal一択

まとめ

  • JavaScriptのDateは設計上のミスやミュータブル性によるバグの温床
  • Temporal APIの登場で、日付・時刻処理の新しい標準が到来
  • 今後はTemporalの利用が推奨され、Dateの時代は終わりを迎える

Hackerたちの意見

ChatGPTの登場で、MDN Date APIのドキュメントへのトラフィックが半分以下になったんじゃないかと思ってる。
正直言うと、もう二度と考えなくて済むなら嬉しいタイプのものだよね。
ユーザートラフィックが半分に減って、クローラートラフィックが10倍に増えるって感じだね。
Date APIはまあまあ良くて、比較的標準化されてるよ。理解しちゃえば、扱うのはすごく簡単。最大の問題は、単純にタイムゾーンをサポートしてないこと。これがTemporalを使う主な理由なんだよね。
ChatGPTを使って、ほぼすべてのmoment/moment-tzに関する質問に答えてもらいながらスケジューラーを作ったよ。長いAPIドキュメントや回答を探すのが得意なんだよね。
このTemporalポリフィルを使ってるんだけど、今のところめっちゃ良い感じだよ: https://github.com/js-temporal/temporal-polyfill
momentや特にluxonと比べてどうなの?
これ51kbあるから、軽量とは言えないね。https://bundlephobia.com/package/@js-temporal/polyfill@0.5.1 それでも、将来の互換性やサーバー用にはいいけど、小さいアプリには結構影響があるかも。
タイポがあるね: // 32から49の間の数値文字列は2000年代だと仮定される: console.log( new Date( "49" ) ); // 結果: Date Fri Jan 01 2049 00:00:00 GMT-0500 (Eastern Standard Time) // 33から99の間の数値文字列は1900年代だと仮定される: console.log( new Date( "99" ) ); // 結果: Date Fri Jan 01 1999 00:00:00 GMT-0500 (Eastern Standard Time) 2番目の区間は33じゃなくて50から始まるべきだね。
APIをチェックしてみたら、Temporal.Durationには年、月、日、...ナノ秒までの多くのパラメータを持つコンストラクタがあるのに、Temporal.Instantは現在の年/月/日から作成する方法が全くないのが驚きだった。ユニックスタイムスタンプ(または文字列)からしか作れないみたい。それって欲しい機能じゃない?それとも、最初に数字を文字列に変換してからTemporal.Instantに戻すつもりなのかな?
コンストラクタのフィールドでローカル値とUTC値を混同してほしくないんじゃないかな(特にローカル値を使うならDSTの遷移も考慮しなきゃいけないし)。
年/月/日だけじゃ、時間の瞬間を一意に特定するには足りないんだよね。でも、その値を使ってTemporal.PlainDate(Time)を作ることはできるし、必要に応じて他のタイプに変換すればいいよ(例えば、タイムゾーンとか)。
Durationコンストラクタで秒、分、時間をデフォルトでゼロにするのは全然理にかなってる。でも、Instantの場合は、タイムゾーンオフセットを指定しない限り、ゼロにするのは意味がないよね。実際、静的メソッドInstant.fromはRFC 9557の文字列を受け入れて、2桁の時間とタイムゾーンオフセットが必要だけど、分や秒は省略できるんだよ:Temporal.Instant.from("2026-01-12T00+08:00")
この記事はDateコンストラクタのいくつかの馬鹿げた点を挙げてるけど、最も許せない点にはほとんど触れてない。記事の例はこうだよ: // もちろん、年、月、日をハイフンで区切らない限り。 // そうすると、_日_が間違ってる。 console.log( new Date('2026-01-02') ); // 結果: Date Thu Jan 01 2026 19:00:00 GMT-0500 (Eastern Standard Time) この例では、日が「間違ってる」のは、コンストラクタの入力が1月2日の真夜中UTCとして解釈されてるからで、その瞬間、東部標準時では1月1日の午後7時なんだよ(著者のローカルタイム)。ここで実際に起こってるのは、エラーのコメディなんだ。JavaScriptはその特定の文字列形式("YYYY-MM-DD")をISO 8601の日付のみの形式として解釈してる。ISO 8601では、タイムゾーン指定がなければ、時間はローカルタイムと見なされるって規定してる。ES5の仕様作成者たちはISO 8601の動作に合わせるつもりだったけど、なぜか「欠損したタイムゾーンオフセットの値は“Z”」(UTC)に変更しちゃったんだ。数年後、彼らは自分たちのミスに気づいて、ES2015で修正しようとした。でも、ブラウザが正しい動作を実装したとき、以前の間違った動作に依存してたウェブサイトからの報告が多すぎて、完全に元に戻されちゃったんだ。「ウェブ互換性」の祭壇に捧げられたわけさ。詳しくは、この記事の下の方にある「Broken Parser」セクションを見てね:https://maggiepint.com/2017/04/11/fixing-javascript-date-web...
>「ウェブ互換性」の祭壇に捧げられた。 じゃあ、彼らは何をすべきだったの?みんなにブラウザのバージョンを検出させて、それに基づいて分岐させるべきだったの?IE5の昔みたいに?(真剣な質問、もしかしたら何か賢いトリックを見落としてるかも。)
これがコメディなら、悲劇にサインアップするよ。これって、無数の小さくて見落としがちなバグの根源になってる気がする。
すごく覚えてるけど、文字列を分割してそのコンポーネントを再構築する関数をコーディングしたことがあったんだ。そうすることで、タイムゾーンなしで日付が作成されることを確認できた。時々、日付はただの日付なんだよね。誕生日は特定の日にあるもので、他の州に引っ越したからって数時間ずれるわけじゃない。昔のOutlookは誕生日を終日イベントとしてマークしてたけど、タイムゾーンの値を保存してたから、ベルギーに保存してた人たちの誕生日は、カリフォルニアに引っ越したらずれちゃったんだよね…
https://jsdate.wtf/で遊んでみるといいよ。JSのDateがどれだけ変なものか、想像もつかないから。
TemporalがChromeの安定版でようやく導入されるなんて、ほんと驚きだよ。もっと早く広く使えるようになってると思ってたのに。
Temporal APIが、ほぼすべての他の日時APIと同じように、うるう秒情報を何の形でも問い合わせるサポートがゼロなのはイライラするね。提案されてる回避策、例えばtemporal-taiは、うるう秒ファイルを差し込んで更新し続ける必要があって、特にクライアントサイドのJSでは痛いんだよね。他のサイトからうるう秒ファイルをダウンロードできないからさ。ブラウザは十分な頻度で更新されるのに、日時APIは「このプロジェクトではUTCだけが対象」とこだわって、うるう秒情報を公開しないんだ。天文計算用のJSツールを書きたいんだけど、UTC変換にはうるう秒情報が必要だから、この流れだと「Just Works™」なものを書くのが不可能なんだよね。
> 「他のサイトからうるう秒のファイルをダウンロードできないのはSOPのせいだって言ってるの?」 SOPがうるう秒ファイルのホスティングを妨げる理由は何なの? ただAccess-Control-Allow-Originを設定すれば、他のサイトがアクセスできるようになるだけなのに。もしくはJSファイルとして提供すれば、ヘッダーも必要ないし。SOPが防いでるのは、他人のうるう秒ファイルをホットリンクして、彼らの帯域を無断で使うことだけだよ。 > その間に、ブラウザは十分な頻度で更新されてるけど、最新のコピーを保ってるのは本当なの? 今のところ、うるう秒データファイルを持ってるブラウザは知らないな。そんなデータファイルを追加して、最新の状態に保つのは、新しいブラウザ開発者にとってはかなり大変な作業だと思うよ。ブラウザが自分で使うことはないのにね。ICU/CLDRファイルみたいに、ブラウザが自分のユーザーインターフェースコンポーネントを描画するために必要なものとは違うから。
> 「でも、日付時刻APIはうるう秒情報を公開しないんだ。なぜなら、"このプロジェクトではUTCだけが対象"にこだわってるから。」 これは少なくとも2つのレベルで意味がわからないよ。まず、厳密に言えば、UTCの定義はうるう秒を含む時間尺度だから、UTCにこだわるならうるう秒もサポートしてることになる。次に、もっと広くあなたの意見に答えると、「彼らは"このプロジェクトではPOSIX時間尺度だけが対象"にこだわりすぎてる」と言うべきだね。それが現状をより正確に捉えていて、問題を暗示してる。特別なアプリケーションを除けば、基本的にすべてがPOSIX時間に基づいて作られていて、うるう秒の存在を無視してるから。
> 不変の値が変数に割り当てられると、JavaScriptエンジンはその値のコピーを作って、メモリに保存する。 それはちょっと違うよ。言語は値がコピーされるかどうかを指定してないし、正確に言うと、値が不変だから、ユーザーがコピーされたかどうかを判断する方法はないんだ。例えば、文字列も不変の値タイプだけど、変数に割り当てたりパラメータに渡すたびに、全体の文字列を完全にコピーするJSエンジンなんてないって確信できるよ。
JavascriptのDateにはたくさんの問題があるけど、オブジェクトであることはトップ10には入らないかな。もしDateオブジェクトが不変だったら良かったかもね。でも、可変オブジェクトを変更するとそのオブジェクトが変わるってことは、驚くべきことじゃないと思うよ。
ちょっと関連するけど、Jiff (https://github.com/BurntSushi/jiff) はTemporal APIにインスパイアされたRustライブラリだよ。