次世代JS標準時刻API Temporal を3年先行利用して得た知見を共有します!
株式会社ミラティブ / 8beeeaaat
メンバー / フロントエンドエンジニア / 従業員規模: 101名〜300名 / エンジニア組織: 51名〜100名
ツールの利用規模 | ツールの利用開始時期 | 事業形態 |
---|---|---|
10名以下 | 2022年1月 | B to C |
ツールの利用規模 | 10名以下 |
---|---|
ツールの利用開始時期 | 2022年1月 |
事業形態 | B to C |
導入の背景・解決したかった問題
導入背景
Date型の課題
従来、JavaScriptの日時表現には Date オブジェクト が利用されてきました。 一方でDateオブジェクトは様々な設計上の課題を抱えています。
- mutableなため意図しないバグを生みやすい
- 同じインスタンスを使い回すと思わぬ副作用が発生することがある
- 精度がミリ秒単位なので、より高い精度が求められるユースケースには不向き
- 複雑な日時計算や入力値の検証に対応させるために外部ライブラリ依存が増える
- 2022年の導入当時はDay.jsを利用
- タイムゾーン・時差の取り扱い管理が難しい
- ユーザーとシステムのタイムゾーンの違いに注意が必要
- Date オブジェクトは常にシステムまたは環境のタイムゾーンで評価され、任意のタイムゾーンを完全に保持することができない
- Day.js等のラッパーライブラリを使っても、Dateに戻すとタイムゾーンが上書きされる
- バックエンド連携時に
toISOString
でISO8601フォーマットで文字列として扱うことで時差(オフセット)は表現可能。一方でタイムゾーンは含まれないため、タイムゾーン付きで時刻表現を行いたい場合は追加の実装が必要となる。
どのような状態を目指していたか
極力Web標準APIを利用し、サードパーティライブラリへの依存を最小化
比較した軸
日時入力が求められるフォームの入力値検証など、既存ライブラリ利用箇所からのリプレイスに伴うコストが低いこと
選定理由
- Temporal がTC39 ProposalのStage3に到達したことで、将来的に標準API化される見通しが立った
- Polyfillを導入することでブラウザ実装を待たずに利用可能な状況だったこと
import { Temporal } from "temporal-polyfill";
const now = Temporal.Now.zonedDateTimeISO();
=> Temporal.ZonedDateTime 2025-09-06T01:23:45.678+09:00[Asia/Tokyo]
Temporalの主な特徴
- immutableで安全に扱える
- 用途別に表現型を選択できる
- タイムゾーン表現・操作にも対応(ZonedDateTime型)
- ナノ秒精度で時刻を扱える
- 日時操作でよく利用するAPIが組み込みで実装されている(算術、比較、日付検証、期間の算出など)
導入の成果
改善したかった課題はどれくらい解決されたか
サードパーティの時刻ライブラリ(Day.js)への依存を無くすことができました。 2025年10月現在、メジャーブラウザのうちで実装が完了しているのは Firefox に限られているためPolyfillの併用は必要ですが、今後その他のブラウザ / JSランタイムでの実装も見込まれているため将来的には併用せずともよくなるものと考えています。
どのような成果が得られたか
3年間先行利用したことで、Temporal普及後を見据えた標準APIによる時刻操作やフォーム入出力の検証実装、バックエンドとの連携について知見を得ることができました。
導入に向けた社内への説明
上長・チームへの説明
サードパーティライブラリへの依存を維持するコストをかけ続けることのデメリットと、標準API化が見込まれる Temporal を先行利用することで次世代の時刻表現の知見を早期に得られるメリットを説明し導入が決定しました。 また不具合が生じた際にエンドユーザーへのインパクトを最小限とするため、一部の社内オペレーション業務アプリケーションから導入を進めることとしました。
活用方法
よく使う機能
現在時刻の取得
const now = Temporal.Now.zonedDateTimeISO();
=> Temporal.ZonedDateTime 2025-09-06T01:23:45.678+09:00[Asia/Tokyo]
Date型との相互変換
Dateからの移行期に便利な相互変換
// Date → Temporal.ZonedDateTime
export function dateToZdt(date: Date, timeZone: Temporal.TimeZoneLike = "Asia/Tokyo") {
return Temporal.Instant.fromEpochMilliseconds(Number(date)).toZonedDateTimeISO(timeZone);
}
// Temporal.ZonedDateTime → Date
export function zdtToDate(t: Temporal.ZonedDateTime) {
return new Date(t.epochMilliseconds);
}
RFC3339フォーマットとの相互変換
時差( OffsetDateTime )を含む表現であれば、RFC3339フォーマットの利用が便利です
// "2025-09-01T12:34:56.789Z" → Temporal.ZonedDateTime
export function rfc3339ToZdt(rfc3339: string, timeZone: Temporal.TimeZoneLike = "Asia/Tokyo") {
return Temporal.Instant.from(rfc3339).toZonedDateTimeISO(timeZone);
}
// Temporal.ZonedDateTime → "2025-09-01T12:34:56.789+09:00"
export function zdtToRFC3339(t: Temporal.ZonedDateTime){
return t.toString({ timeZoneName: "never", calendarName: "never" });
}
input["datetime-local"]との相互変換
HTMLフォームのdatetime-local型の ローカル日時文字列 の値を取り扱う際に活躍します
// "2025-10-01T12:34:56" → Temporal.ZonedDateTime
export function inputDateTimeToZdt(dateTimeString: string, timeZone: Temporal.TimeZoneLike = "Asia/Tokyo") {
return Temporal.PlainDateTime.from(dateTimeString).toZonedDateTime(timeZone);
}
// Temporal.ZonedDateTime → "2025-10-01T12:34:56"
export function zdtToInputDateTime(t: Temporal.ZonedDateTime) {
return t.toPlainDateTime().toString({ smallestUnit: "second" });
}
日本語日時表現
option引数に Intl.DateTimeFormat を加えて実務でよく利用される日本語表記に変換します。
// Temporal.ZonedDateTime → "2025年10月1日(水) 10:00:00"
export function zdtToLocalizedDateTime(
t: Temporal.ZonedDateTime,
options?: Intl.DateTimeFormatOptions,
): string {
return t.toLocaleString("ja-JP", {
year: "numeric",
month: "long",
day: "numeric",
weekday: "short",
hour: "numeric",
minute: "numeric",
second: "numeric",
...options,
});
}
バリデーションでの利用例: Zodスキーマ連携
import { z } from "zod";
import { inputDateTimeToZdt, isBefore } from "./util";
const schema = z.object({
StartsAt: z.string().transform((val) => inputDateTimeToZdt(val)),
EndsAt: z.string().transform((val) => inputDateTimeToZdt(val)),
})
.refine((values) => {
if (values.StartsAt.equals(values.EndsAt)) return false;
return isBefore(values.StartsAt, values.EndsAt);
}, {
message: "開始時間は終了時間より前に設定してください",
path: ["StartsAt"],
})
ツールの良い点
- 慎重な取扱が必要だったDateの課題を標準APIで解決することができる
- 過剰なサードパーティライブラリへの依存を減らすことができる
ツールの課題点
- Dateが単一の型であるのに対し、Temporalは利用箇所の用途に応じて適切に型を使い分ける思想であるため、機能設計時に各実装者がどの型を利用するか検討するコストが発生する
ミラティブでは先述したようなヘルパー関数を用意した上で Temporal.ZonedDateTime をベースの型として利用することで、アプリケーション内の時刻データはタイムゾーン付きオフセット時刻で管理するように統一しています。
ツールを検討されている方へ
バックエンドとの連携など、Temporalの活用事例について詳細を知りたい方は弊社テックブログをご参照ください。Xもやっていますのでお気軽に感想をお寄せください!
次世代JS標準時刻API Temporal を3年先行利用して得た知見を共有します! - Mirrativ Tech Blog
今後の展望
Temporal の普及により、日時処理の安全性・可読性・精度が大きく向上することが期待されています。標準化、各ブラウザでの実装が待ち遠しいですね! Date型からの移行を行う場合は、是非例示した変換器などをご参考に段階的に移行を検討してみてください。
株式会社ミラティブ / 8beeeaaat
メンバー / フロントエンドエンジニア / 従業員規模: 101名〜300名 / エンジニア組織: 51名〜100名
株式会社ミラティブ / 8beeeaaat
メンバー / フロントエンドエンジニア / 従業員規模: 101名〜300名 / エンジニア組織: 51名〜100名
レビューしているツール
目次
- 導入の背景・解決したかった問題
- 活用方法