🕰️

Temporal APIの現在地(2025年2月時点)

2025/02/27に公開

はじめに

Temporal APIは、一言で要約すると「JavaScriptの日付・時間の処理をまともにする」ためのプロポーザル(仕様提案)です。

この記事では、Temporalについて「今どういう状態なのか」「今すぐ使うにはどうすればいいのか」「今すぐ使う場合に注意すべきことは何か」といった情報を提供します。

仕様

Temporalは現在stage 3です。

https://github.com/tc39/proposal-temporal

2024年6〜9月にかけて巨大な仕様変更(normative change)があったものの、2025年現在Temporalは仕様として「ほぼ完成」しています。2つ以上のJavaScript処理系に実装され次第stage 4になり、ES202xとして標準化される予定です。

https://zenn.dev/cybozu_frontend/articles/temporal-reduces-scope

ドキュメント

最近、MDNにTemporal関連のドキュメントが一気に追加されました。リファレンスとしてはこれを見るのがいいでしょう。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal

また、プロポーザルのドキュメントに具体的なコードの例がいろいろ載っています。

https://tc39.es/proposal-temporal/docs/cookbook.html

JavaScriptエンジン

SpiderMonkeyが(ほぼ)実装を完了していて、Firefox Nightlyで利用できます。

V8とJavaScriptCoreは道半ばです。2025年2月のTC39 meetingの資料によると、2月時点でV8は74%、JavaScriptCoreは40%のテストが通っているそうです(この割合は必ずしも進捗状況と対応していないことに注意)。仕様が巨大で明らかに実装がめちゃくちゃ大変なので、気長に待ちましょう。

https://ptomato.name/talks/tc39-2025-02/#13

一応Denoでは--unstable-temporalフラグを付けるとTemporalを使えるのですが、そもそもV8の実装が全然終わってないので平気でsegmentation faultしたり変なエラーが出たりします。個人的には大人しくpolyfillを使った方がいいと思います。

ところで、Ladybird(正確にはそのJSエンジンであるLibJS)がTemporal関連テスト通過率でSpiderMonkeyに次いで2位(97%)なのがちょっと面白いです。

懸念

ところで、筆者がプロポーザルのリポジトリや各種polyfillやFirefoxのバグトラッカーを巡回する中で、暦(calendar)の計算の複雑性と実装間の差異が問題として表面化しつつあると感じています。

https://bugzilla.mozilla.org/show_bug.cgi?id=1912511

たとえば、現状のFirefox Nightlyの実装において、一部の暦を利用した際にIntl.DateTimeFormattoLocaleStringで出力した日付文字列とTemporalで得た日付データが食い違うなどの問題が発生しています。バグを直すのには時間がかかります[1]が、バグを放置したまま通常版Firefoxに機能を展開することも望ましくないと、Firefoxの開発者たちは考えているようです。ただでさえ仕様策定・実装が遅れているTemporalを可能な限り早くリリースするために特定の暦のサポートを暫定的に無効化する選択肢も検討されていますが、私の仕様書読解が正しければ、Intl.DateTimeFormatで利用できる暦をTemporalでサポートしないことは厳密には仕様違反に当たります[2]

こういった問題はTemporal実装のフラグなしリリースや仕様の確定、ひいてはstage 4昇格のブロッカーになり得ます(ならないかもしれませんが)。

polyfill

JavaScriptエンジンによるサポートが道半ばなので、今すぐプロジェクトで使いたい場合はpolyfillを利用することになります。

今のところ、実用的なpolyfillは2つあります。

@js-temporal/polyfillはプロポーザルのリポジトリにある仕様確認用polyfillをベースにプロポーザルのchampionが開発していて、temporal-polyfillfullcalendarというライブラリの開発者がメンテナンスしています。

この2つのpolyfillの最大の違いはバンドルサイズです。temporal-polyfillがgzipで20kbなのに対し、@js-temporal/polyfillはgzipで48kbです[3]。また、temporal-polyfillではプロパティディスクリプタや関数のlengthプロパティが仕様と異なることがあるようです。

2025年2月27日現在、どちらのpolyfillも最新の仕様に追従できていません。アップデート作業自体は進んでいるので、そのうち更新されるとは思います(いつになるかは分かりませんが)。

https://github.com/js-temporal/temporal-polyfill/issues/288
https://github.com/fullcalendar/temporal-polyfill/issues/47#issuecomment-2679163063

polyfillをグローバルに読み込んで使うのは今のところ時期尚早なので、通常のライブラリと同様にimportして、グローバル環境を書き換えることなく(ponyfill的に)利用するのが良いのではないでしょうか。

// 通常のライブラリ同様に使える
import { Temporal } from 'temporal-polyfill';
console.log(Temporal.Now.zonedDateTimeISO().toString());

どのタイミングで一般的なpolyfillとしての利用を解禁するかは悩ましいところです。polyfillが最新の仕様に追い付くまで待つのは大前提として、少なくとも通常版Firefoxで利用できるまで待った方が、仕様と実装の安定の観点から望ましい気はします[4]。もう少し慎重を期してstage 4になるまで待つのも十分アリです。

どのpolyfillを使うべきか?

今すぐ採用するのであれば、より新しい仕様を反映しているtemporal-polyfillの方がいいと思います。temporal-polyfillと現在の仕様との差異は基本的に「仕様から削除された関数が残っている」という点だけなので、MDNのドキュメントを見ながらコードを書けば基本的に問題になることはありません。

ただし、仕様変更の過程でTemporal.ZonedDateTime.prototype.getTimeZoneTransition()メソッドが追加されたので[5]、このメソッドは今のpolyfillに存在しません。「次または前のタイムゾーンのオフセット切り替えがいつか知りたい」というユースケースは稀だと思いますが、どうしてもgetTimeZoneTransitionメソッドが今すぐ必要な場合、次のようなコードを書いてpolyfillにモンキーパッチを当てることができます。

function getTimeZoneTransition(
  this: Temporal.ZonedDateTime,
  direction: "next" | "previous" | { direction: "next" | "previous" },
): Temporal.ZonedDateTime | null {
  const dir = typeof direction === "string" ? direction : direction.direction;
  const timeZone = this.getTimeZone();
  const instant =
    dir === "next"
      ? timeZone.getNextTransition?.(this.toInstant())
      : timeZone.getPreviousTransition?.(this.toInstant());
  if (instant) {
    return instant
      .toZonedDateTimeISO(timeZone)
      .withCalendar(this.getCalendar());
  } else {
    return null;
  }
}

Temporal.ZonedDateTime.prototype.getTimeZoneTransition = getTimeZoneTransition;

他にもめちゃくちゃ特殊なエッジケースに対する修正が未反映だったりしますが[6]、こちらも現実的に問題になることはまずないでしょう。

さて、将来的に両方のpolyfillが仕様通りに更新された場合、挙動は一切変わらないはずなのでどちらを選んでも問題になりません。ただし、フロントエンドで使う場合は、バンドルサイズの関係でtemporal-polyfillを使うことになるでしょう。temporal-polyfillとてgzipで約20kBなのでさほど小さいわけではないのですが、@js-temporal/polyfillは50kB弱です。後述するようにTemporal自体は日時操作ライブラリとして基本的な機能しか持たないので、それで50kBはちょっと大きすぎるという印象があります。

複数のTemporal実装が併存してしまうリスク

Node.jsにおけるdual package hazard[7]、異なるバージョンのpolyfillの混在、複数のpolyfillやネイティブ実装の混在などが原因で、別の実体を持つTemporal実装が併存してしまう可能性があります。

引数としてTemporalのインスタンスを受け取るメソッドは全て「もしインスタンスの内部状態を読み取れなかったら通常オブジェクトとしてインスタンスに変換する」というステップを挟むので[8]、よほど変なこと(実装Aのインスタンスに実装Bのメソッドをbindして呼び出すとか)をしない限り動作が壊れたりはしません。しかし、instanceofを使ってインスタンスの型を判定したりしていると、Temporalクラスの実体が複数ある場合に動作がおかしくなります。

自分たちのプロジェクトだけで使うだけならいずれかの実装に統一すればいいですが、後述するように外部のライブラリを使う場合は要注意です。

ライブラリ

Temporal APIは、少なくとも現状ではmomentやLuxonといった日時操作ライブラリを置き換えるものではありません。

基本的な機能、たとえば日時や期間の足し引き、比較、丸め操作などはメソッドとして提供されています。しかし、たとえば「ある日付が日付Aと日付Bの間にあるか」を判定するメソッドはありません。これらは基本的な機能を組み合わせれば実装できますし、「便利機能はTemporal APIの上のライブラリ層で提供してください」という態度は言語の標準機能としては合理的です。手当たり次第に「あれば便利」レベルのAPIを入れたら言語機能が肥大化します。

では肝心のライブラリはどれくらい充実しているかというと、luxonやdayjsやdate-fnsみたいな定番が定まっていないどころか、そもそもライブラリがほとんどありません。

私はnpmとjsrで「temporal」で検索し、publish日時が新しい順に3年分くらい全てのパッケージを確認しましたが、現時点のTemporalに対応したライブラリは僅かしか存在しませんでした。私が見落としている可能性はありますが……

https://github.com/rhnorskov/temporal-fns
https://github.com/macalinao/temporal-utils
https://github.com/ayame113/temporarily
https://github.com/fabon-f/vremel

たぶん現状では4つ目のvremelが一番いいんじゃないでしょうか。これは私が開発しているライブラリなので手前味噌になってしまいますが、可能な限り客観的に比較してもなお最も機能が多く、さらに最も望ましい設計になっていると思います。

  • 2種類のpolyfillのどちらを使っても(あるいは今後実装されるかもしれない別のpolyfillを使っても)、はたまたブラウザに今後実装されるネイティブのTemporal APIを使っても動く
  • 勝手にpolyfillを読み込まない
  • globalThis.Temporalをライブラリから一切参照していないので、polyfillをグローバルに読み込まなくても使える
  • 古い仕様に従っている現状のpolyfillはもちろん、最新の仕様に従っているFirefox Nightlyの実装にも対応している(はず)
  • TypeScriptの型も新旧の仕様の両方に対応している[9]

どのようなライブラリを使うにしろ、前述の「実装混在リスク」を回避するため、ライブラリがTemporalを独自に読み込んでいる場合はライブラリ側とプロジェクト側とで利用しているTemporalが同一オブジェクトであることを確認するべきです。

とはいえ、将来的にはpolyfillがglobalThis.Temporalを追加するようになり、ライブラリもユーザーもglobalThis.Temporalを唯一のTemporalインスタンス生成元として扱うようになるでしょう。これは過渡期ゆえの混乱であり、stage 4に上がる頃にはエコシステム全体がもうちょっといい感じになっていることを願っています。

まとめ

正直なところ、Temporalは今すぐ実用するのが少し難しい段階にあります。とはいえ近い将来に標準化されることは間違いありません(具体的な時期はまだ分かりませんが)。それに、今のところstage 3とはいえ、趣味のプロジェクトで使える程度には十分成熟しています。

ぜひ試してみてください。

脚注
  1. Firefoxの場合、Intl.DateTimeFormatではICU4Cを、TemporalではICU4Xを利用しているため、2つのライブラリの差異がバグとして表出しているようです。FirefoxではICU4CからICU4Xに移行する長期計画を立てているので、Intl.Segmenterと同様に新規実装でICU4Xを使うのは理解できます(cf. https://zenn.dev/cybozu_frontend/articles/explore-intl-segmenter)。とはいえ、今からTemporalをICU4Cで書き直すのも、Intl.DateTimeFormatterをICU4CからICU4Xに移行するのも、ICU4CとICU4XとCLDRで相談して挙動を統一するのも、たぶん時間がかかって相当大変でしょうし…… ↩︎

  2. AvailableCalendars抽象操作はIntl.DateTimeFormatで利用できるcalendarの一覧を返すように定められています(https://tc39.es/proposal-temporal/#sec-availablecalendars)。さらに、Temporal.PlainDateなどを生成する際に「指定されたcalendarAvailableCalendarsのリストに含まれていない」以外の理由でエラーを発生させられるステップは存在しません。 ↩︎

  3. バンドルサイズの測定方法と詳細な結果は https://github.com/fabon-f/temporal-polyfill-comparison に書いてあります。また、直近の仕様変更は機能削減だったので、最終的にどちらのpolyfillもサイズがもう少し小さくなるはずです。 ↩︎

  4. Temporalの巨大な仕様に対してtest262のカバレッジは今のところ十分とはいえません。プロポーザルのリポジトリにあるレファレンス実装すら、テストで発見できなかった細かいバグが散発的に報告・修正されています。polyfill実装の成熟にも多少の時間が必要かもしれません。 ↩︎

  5. 正確には追加というより移管です。以前の仕様にはTimeZoneインターフェース(プロトコル)とそれを実装したTimeZoneクラスが存在し、前後のオフセット切り替えを返すメソッドはそちらに属していました。しかしTimeZoneインターフェース(プロトコル)とTimeZoneクラスが仕様から削除された結果、元のメソッドがZonedDateTimeに移動されて今に至ります。 ↩︎

  6. 具体的には https://github.com/tc39/proposal-temporal/pull/2916https://github.com/tc39/proposal-temporal/pull/2918https://github.com/tc39/proposal-temporal/pull/3054 です。1つ目は現実的に意味のないエッジケースに対してエラーを返すようにするもの、2つ目はTZDBに1例(100年以上前のトロント)だけ記録されている特殊なオフセット切り替えを正しくサポートするもの、3つ目はTemporal APIで扱える範囲外の日付文字列からPlainMonthDayを生成しようとした際の挙動を定めるものです。他にもいくつかnormative changeがありましたが、全てtemporal-polyfillでは先行して修正されているので結果的に仕様通りの挙動になっています。 ↩︎

  7. もちろんユーザー側で気をつけるよりはpolyfill側で対応する方が遥かに望ましいので、Node.jsのrequire(esm)を利用したdual package hazardの解決を提案しています(https://github.com/fullcalendar/temporal-polyfill/issues/62)。 ↩︎

  8. ただしTemporal.Instantはオブジェクトではなく文字列を経由します。なのでTemporal.Instant.from({ toString() { return "2025-01-01T00:00:00Z" } })はエラーになりません。まあ、こんな変なことをする人はいないでしょうけど。 ↩︎

  9. さらっと書いてはいますが、これらの挙動を実現するのは想像以上に面倒でした。グローバル変数やinstanceofに頼らずSymbol.toStringTagによってTemporalインスタンスの種類を判別したり(https://github.com/fabon-f/vremel/blob/9efd66d1fc733d1ea3bac2dc416252eb02124033/src/type-utils.ts)、最新の仕様の型定義を元に新旧の型の両方に互換性のあるサブセットを定義したり(https://github.com/fabon-f/vremel/blob/9efd66d1fc733d1ea3bac2dc416252eb02124033/src/temporal.d.ts)、Temporalのインスタンスを返す関数では必ずTemporalの型をextendしたジェネリクス型を利用したり、必要に応じて返すべきTemporalのクラスを引数として渡してもらったりと、割と泥臭いワークアラウンドを使いまくっています。 ↩︎

Discussion