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

Temporal: JavaScriptにおける時間を修正するための9年間の旅

概要

  • Bloomberg はJavaScriptの標準化やインフラに深く関与
  • Date API の歴史的課題とその限界
  • Moment.js などのライブラリ依存による新たな問題
  • Temporal 提案の誕生と発展の経緯
  • Temporal API の特徴と現状

BloombergとJavaScript標準化の歩み

  • Bloomberg はJavaScriptの利用と標準化に積極的に関与
  • 社内エンジニア向けに JavaScript環境 を提供
  • 2018年以降、 TC39 会議に参加し、RealmsやWebAssemblyなどの議論に貢献
  • Igalia との協力による標準化推進
    • Arrow Functions、Async Await、BigInt、Class Fields、Promise.allSettled、Promise.withResolvers、WeakRefs、Source Maps標準化支援
  • Promise.allSettledTemporal 提案への貢献

JavaScriptの進化と標準化プロセス

  • JavaScriptは ブラウザ横断 で動作、単独での仕様変更不可
  • TC39 (ECMAScript技術委員会)で進化
  • 提案は複数の 成熟度ステージ を経て標準化
    • Stage 0: アイデア
    • Stage 1: 問題領域の承認
    • Stage 2: 設計案の選定
    • Stage 2.7: 原則承認、テスト・フィードバック待ち
    • Stage 3: 実装とフィードバック
    • Stage 4: 標準化

Date APIの歴史的背景と課題

  • 1995年、 Brendan Eich がMocha(JavaScriptの前身)を10日で開発
  • JavaのDate実装 をそのまま移植
  • 当時はJavaScriptをJavaの軽量版として設計
  • APIの変更は政治的にも困難、 一貫性優先
  • Webが進化しても、 Date API はほぼ変化なし

Dateの主な問題点

  • ミュータブル (変更可能)なオブジェクト設計
    • 意図せず元のDateを変更してしまう
  • 月単位の計算の一貫性欠如
    • 例:1月31日に1ヶ月加算→3月2日になるなど直感に反する挙動
  • 曖昧なパース
    • 仕様外の文字列で挙動がブラウザごとに異なる
    • ローカルタイム・UTC・エラーのいずれかになる不確定性

ライブラリエラの到来と課題

  • Moment.js などのライブラリがDateの欠点を補完
    • 強力なパース、イミュータブルなAPI、表現力の高い操作
  • 100万回以上の 週次ダウンロード を記録
  • ライブラリ導入で バンドルサイズ肥大 問題
    • ロケール・タイムゾーン情報の同梱不可避
    • Tree-shakingや最適化でも不要データの除去が難しい

Temporal提案と推進体制

  • Maggie Johnson-Pint らが2017年に Temporal Proposal をTC39へ提出
  • Stage 1到達後、要件整理や設計の明確化など地道な作業が続く
  • Bloomberg の要件
    • ユーザーごとのタイムゾーン設定
    • IANA Time Zone Databaseによる正確な歴史的タイムゾーン挙動
    • ナノ秒単位の高精度タイムスタンプ
  • IgaliaGoogleMicrosoft など多様な関係者が協力
  • Champion メンバー:Maggie Johnson-Pint、Matt Johnson-Pint、Brian Terlson、Richard Gibson、Philipp Dunkel、Ujjwal Sharma、Philip Chimento、Jason Williams、Shane Carr、Justin Grant

Temporal APIの概要と特徴

  • Temporal はグローバルスコープのトップレベル名前空間オブジェクト
  • MathやIntl同様、 複数の型 (コンストラクタ)を内包
  • 代表的な型: Temporal.ZonedDateTime
    • Dateの概念的な後継
    • 明示的なタイムゾーンとカレンダーサポート
    • 完全なイミュータブル設計
    • サマータイムなどの複雑な計算も正確に対応
  • 例:現在日時の取得
    • const now = Temporal.Now.zonedDateTimeISO();
  • ZonedDateTime型は日時計算時にタイムゾーンやサマータイムの遷移を考慮
    • 例:ロンドンのDST開始時の加算操作

まとめ

  • Date API の限界を克服するために Temporal が標準化
  • イミュータブル設計高精度サポート明確なタイムゾーン管理
  • 業界横断の協力体制による長年の課題解決
  • JavaScript開発者 にとって、今後の標準的な日時操作手段となる見込み

Hackerたちの意見

サーバーサイドのランタイムに導入されるのが待ちきれない!これがあれば、全面的に採用できるんだけどな。

Node 26!時間の問題だね… :)

参考までに、私はしばらくの間、js-temporalポリフィルを使ってサーバーサイドで使ってるけど、問題はないよ。

Denoはもうかなりのマイナーバージョンで--untable-temporalフラグの裏にあったけど、最新のマイナーアップデート(TC-39のステージ4受理とV8自体がAPIを安定版としてマークしたから)でフラグの要件がなくなって、すぐに使えるようになったよ。

Temporalで最も評価されていないデザインの決定は、すべてを不変にしたことだと思う。これまでにデバッグした日付関連のバグの半分は、共有されたDateオブジェクトに対してsetMonth()やsetHours()を呼び出したことから来てる。残りの半分は、Dateコンストラクタでの暗黙のローカルタイムゾーン変換が原因。Temporalは、ZonedDateTimeとPlainDateを使ってタイムゾーンを明示的に扱わせることで両方を解決し、新しいオブジェクトを返すことで変更を避けている。9年は長いけど、TC39の提案が半端な状態で出てきて、その後に修正提案が必要になることが多いから、これだけはちゃんとやってほしいな。

それ、実はTemporalで最も評価されているデザインの決定かもしれないね ;) どちらにしても、私も大ファンだよ。

不変性は一般的に過小評価されてるよね。非クロージャーのコードを扱うたびに痛感するよ。

最悪なのは、値を変更して返すメソッドだね。これはコンピュータサイエンスの複雑な領域に入るから、あんまり理解できてないけど、TypeScriptで「この関数に渡されたオブジェクトは今や型が_never_です。壊しちゃったから、これ以降は使えません」って定義できたらいいのに。便利さとパフォーマンスの理由から、関数内で何かを変更して返したいこともあるけど、返された型について考えさせて、元の型には二度と触れないようにしたいんだ。たとえ同じオブジェクトでもね。

これ、10年以上前にJavaで時間を革命的に変えたJoda Timeから取ったみたいだね。残念ながらJodaのことには触れられてないけど。

他の半分は、Dateコンストラクタでの暗黙のローカルタイムゾーン変換から来てる。古いC++(だと思う)バージョンでもその問題を見てみて。ロンドンにいるときに友達の誕生日を3月11日として保存したとする。今はSFにいるけど、友達の誕生日はいつ?それはまだ終日3月11日で、3月10日の午後5時から始まって、3月11日の午後5時に終わるわけじゃない。

JavaScriptを書くときは、できるだけ多くのものを不変にするようにしてる。時々冗長になって、効率的な計算パターンが減ることもあるけど、全体的には理解しづらいバグに直面することがずっと少なくなると思ってる。Temporalのデザインにはあまり好きじゃないところもあるけど、不変性は良い判断だった。理解できないのは、なぜ文字列フォーマットをそんなに厳格にしたのかってこと。国際化に関係してるのかな?レンダリングされた日付・時間の文字列をもっと簡単に作れるようなテンプレートシステムがあったらよかったな。

C++ARM時代にC++で最初に感謝するようになったことの一つは、可変性をモデル化する能力だった。もちろん、他の言語はそれをもっと上手くやってるけど、問題はまだ広まっていないことだね。

可変な共有状態が問題になるのは時間管理だけじゃないよ。特にPythonでは他でも問題が起きてるのを見たし、CやC++でも同様だと思う。多分、Pythonは参照渡しが暗黙的だから、CやC++はポインタや参照がもっと明示的で、そういうエラーのリスクが減るんだろうね。これについてはいくつかの考え方がある。一つは、(ほぼ)すべてを不変にして、最適化されることを期待する方法。これは関数型言語(および関数型スタイルのプログラミング全般)で取られているアプローチだよ。もう一つはRustのやり方で、状態を可変か共有かのどちらかにするってこと。だから、独占的に持つ可変状態か、共有される読み取り専用の状態のどちらかを選べる。どちらのアプローチも有効で役立つと思う。低レベルのパフォーマンス重視のコードを扱っている者としては、個人的にはRustのアプローチが好きだな。

そうだね…キャリアの初期に、日付と時間に関していくつかのことをしっかり固めたんだ。日付と時間 + ゾーン/ロケーションの詳細か、常にUTCでISO-8601スタイル(後にJSONが採用したデフォルト)で通信するかのどちらかだね。ほとんどのシステムとJSはUTCとローカルの間で簡単に変換できるから。ストレージの詳細も同じ。ファイルやログの命名に8601スタイルを使い始めたのは、常に正しくソートされるためで、これがJSON仕様の前からコードの使用に持ち込まれたんだ。こうすることで多くの頭痛を避けられるよ…それに、共通のフォーマットや日付変更のためにいくつかのユーティリティスクリプトを使うことにしてる(今は主にdate-fnsを使ってるけど)、それらは常にdtm = new Date(dtm);で始まって、操作の前にクローンされたdtmを返すようにしてる。

Temporalが受け入れられて本当に嬉しい!長い間頑張ってきたチャンピオンたち、おめでとう!ここ数年、temporal_rsに取り組むのが楽しかったよ :)

Javaの時間APIを改善する旅とつなげるのも面白かったかも。Joda-TimeからJSR 310に進化して、2014年にJava 8と共にリリースされたよね。不変の表現、インスタント、適切なタイムゾーンサポートなど。この記事が「過激な提案」としてこれらの機能をJavaScriptに持ち込むことを言及しているけど、Javaの解決策も影響を与えたんじゃないかな?

JodaがMoment.jsに影響を与え、それがさらにTC39に影響を与えたと考えるべきだね。今日の全体会議で合意を得る際に話したけど、日付と時間のプリミティブを実装または改良するプログラミング言語は、その瞬間に存在するすべての先行技術の恩恵を受けるんだ。TC39は他のエコシステムが何をしているかを広く調査するけど、その足跡を追う必要はなく、JavaScriptにとって最適なものに合意を得るんだ。だから、私の見解では、これは委員会が9年間かけて設計したAPIの最も完全な実装を表していて、2026年に最終決定されると思う。

そうそう、JavaScriptもJavaから悪いバージョンを引き継いじゃったね! https://news.ycombinator.com/item?id=42816135

Safari(テクノロジープレビューで部分的にサポート)Safariが2020年以降のIEの精神的後継者として確認された。

2026年、まだモバイルSafariでネイティブの日付ピッカーのサポートなし。

新機能の実装が遅くなってるけど、まだ実装してるから新しいFirefoxって感じだね。IEの大きな問題は、新機能の実装を止める前にどれだけ人気があったかってこと。まるでGoogleがChromeに飽きて、資金を全て止めたみたいなもんだ。そうなると、投資が止まった後も何年もChromeに縛られたままになる。周りにはChrome特有のもの(Electron、Puppeteer、Seleniumなど)がたくさんあるからね。今のところ、世界はSafariやFirefoxのユーザーがChrome専用のサイトやツールに文句を言う方が必要だと思う。Safariの問題は一時的なものだから。Chromeは新しい皇帝で、IEが悪かったのは止まったからじゃなくて、皇帝でいた後に止まったからなんだ。人々は帝国が崩れた後の悪い時代を覚えてるけど、IEが他のものを巻き込んで崩れたから、IEが崩れた後の混乱の方が記憶に残りやすいんだよね。「IE専用のウェブサイトはビジネスに十分」っていう考えが良いアイデアだった頃を思い出すのは難しい。

いい方向に進んでるけど、APIはまだ好きじゃない。理由はこれ:特にJavaScriptでは、クライアントとサーバー間でコードをたくさん共有するから、データとロジックを厳密に分けたいんだ。つまり、私のデータはプレーンなJSONで、関数プロパティを持つクラスインスタンスやオブジェクトじゃないから、簡単にシリアライズ/デシリアライズできるようにしたい。Temporalオブジェクトはそうじゃないからね。それに、Temporalオブジェクトには関数が付いてるけど、便利ではあるけど、データを送るのが面倒なんだ。純粋な関数のセットがあって、データだけのTemporalオブジェクトを渡せる方がいいな、date-fnsみたいに。

でも、すべてのTemporalオブジェクトは簡単に(デ)シリアライズできるよ。.toStringTemporal.fromはすごくうまく機能する。

これは本当に面倒な問題で、データがシリアライズ境界を常に越えるシステムで同じような緊張感に直面してる。JSON.parse/stringifyで説明してるプロトタイプ剥奪の問題は、もっと一般的な問題の特定のケースだね。リッチなドメインオブジェクトは、再構成ステップなしではワイヤートランスファーを生き残れない。とはいえ、Temporalチームの判断は正しかったと思う。日付と時間のロジックは、「データの袋と自由な関数」のアプローチが微妙なバグを引き起こすドメインの一つだから、呼び出し元が正しいコンテキスト(カレンダーシステム、タイムゾーン)を正しい関数に渡すのを忘れがちなんだ。操作をオブジェクトにバインドすることで、型システムがPlainDateがZonedDateTimeとして誤って扱われないように強制できる。date-fnsは素晴らしいけど、それはできない。シリアライズの問題は境界で解決できるよ。tRPCやそれに似たものを使っているなら、Temporal.Whatever.from()を入るときに呼び出して、.toString()を出るときに呼び出す薄い変換レイヤーは、かなり最小限のオーバーヘッドだよ。Decimal型やJSONを通さない値オブジェクトで使われるのと同じパターンだね。面倒だけど、代わりにAPIの型安全性を放棄することになるから、最初から持っている価値がなくなっちゃう。

date-fns(または似たようなライブラリ)を使い続けることはまだできるよね?

それには賛成だよ。大きなTemporalプロジェクトにちょっと関わったことがあるけど、コードベースの多くがただのプロパティを一層から次の層にマッピングするだけだったのが本当に嫌だった。

特にJavaScriptでは、クライアントとサーバー間で多くのコードを共有することが多いから、データとロジックを厳密に分けるのが好きなんだ。WASMとのインターフェースがどうなるか気になるな。Dateより良いのかな?

これは意図的な設計の決定だったんだ。すべての時間型がシリアライズ/デシリアライズ可能であることを確認したかったんだけど、君が言ったように、JSON.parseはそれをサポートしていないから、最初のオブジェクトに暗黙的に戻ることはできなかったんだ。だから、開発者が必要な正しいオブジェクトを再作成する責任があるんだ。これが問題だとは思わないよ。もし一方からDate、DateTime、MonthDay、YearMonth型を送っていることがわかっていれば、ISO文字列からどの型を再構築すればいいかもわかるからね。自動的にやると、予期しない値を受け取ったときに間違った型を扱うことになるかもしれないから、それが問題になるかもしれない。ドキュメントにはTemporal.Instantのためのリバイバーの例があるよ。https://tc39.es/proposal-temporal/docs/instant.html#toJSON

「それはケン・スミスによるJavaのDateコードをCに移植したもの("Mocha"の中で私が書いていない唯一のコード)」 これ、面白いね。Javaのutil.Dateはほぼ確実にCのtime.h APIのポートだったんだろうな!

彼らは私たちのために9年も未来に進んでこれをやってくれたんだ。感謝してるよ。

余談だけど、Promise.allSettledの大ファンなんだ。それが出たとき、当時書いてたコードがすごくスッキリしたよ。

caniuseの結果を見てると…くそったれなSafari(とOpera)… https://caniuse.com/temporal

普段はSafariの新機能の実装についてあまり厳しく言わないんだけど、これは残念だし、あまり良い印象を与えないね。