Upgrade to Pro — share decks privately, control downloads, hide ads and more …

さようなら Date。 ようこそTemporal! 3年間先行利用して得られた知見の共有

Avatar for 8-beeeaaat!!! 8-beeeaaat!!!
September 06, 2025

さようなら Date。 ようこそTemporal! 3年間先行利用して得られた知見の共有

フロントエンドカンファレンス北海道2025
https://www.frontend-conf.jp/

Avatar for 8-beeeaaat!!!

8-beeeaaat!!!

September 06, 2025
Tweet

Other Decks in Programming

Transcript

  1. JS初学者への洗礼 かつてあなたも通った道 9月を作ったつもりが10月に! new Date(2025, 9, 1) => Wed Oct

    01 2025 00:00:00 GMT+0900 存在しない日付が未検証で処理される! new Date(2025,1,29) // 2025/2/29 は存在しない => Sun Mar 01 2025 00:00:00 GMT+0900 フロントエンドカンファレンス北海道2025 LT @8beeeaaat 5
  2. Temporal APIがやってくる! ECMAScript標準(を目指す)新しい日時API 現在はTC39 Stage 3 Draft。標準化 Stage 4 が見えてきた。

    2025年5月: Firefox139 で正式実装リリース polyfillで利用可能: ミラティブでは2022年から社内管理ツール中心に先行導入 主な特徴 immutableで安全に扱える タイムゾーンに対応。困ったら日時はZonedDateTimeで扱う ナノ秒精度で時刻を扱える 日付検証機能が組み込みで用意されている(オプションで有効にする必要あり) フロントエンドカンファレンス北海道2025 LT @8beeeaaat 6
  3. 今日から始めるTemporal メジャーなpolyfillが2つあるのでお好みで @js-temporal/polyfill fullcalendar/temporal-polyfill の方が軽量 import { Temporal } from

    "temporal-polyfill"; const now = Temporal.Now.zonedDateTimeISO(); => Temporal.ZonedDateTime 2025-09-06T01:23:45.678+09:00[Asia/Tokyo] フロントエンドカンファレンス北海道2025 LT @8beeeaaat 8
  4. よく使う自作変換 Dateとの相互変換 移行過渡期は特に重要。 // Date => Temporal.ZonedDateTime export function dateToZdt(

    date: Date, timeZone: Temporal.TimeZoneLike = "Asia/Tokyo" ): Temporal.ZonedDateTime { return Temporal.Instant.fromEpochMilliseconds(Number(date)).toZonedDateTimeISO(timeZone); } // Temporal.ZonedDateTime => Date export function zdtToDate(t: Temporal.ZonedDateTime): Date { return new Date(t.epochMilliseconds); } フロントエンドカンファレンス北海道2025 LT @8beeeaaat 9
  5. よく使う自作変換 RFC 3339 文字列との相互変換 バックエンドとの疎通で利用機会多し(後述) // "2025-09-01T12:34:56.789Z" => Temporal.ZonedDateTime export

    function rfc3339ToZdt( rfc3339: string, timeZone: Temporal.TimeZoneLike = "Asia/Tokyo" ): Temporal.ZonedDateTime { return Temporal.Instant.from(rfc3339).toZonedDateTimeISO(timeZone); } // Temporal.ZonedDateTime => "2025-09-01T12:34:56.789+09:00" export function zdtToRFC3339(t: Temporal.ZonedDateTime): string { return t.toString({ timeZoneName: "never", calendarName: "never" }); } フロントエンドカンファレンス北海道2025 LT @8beeeaaat 10
  6. よく使う自作変換 ローカル日時文字列 との相互変換 input["datetime-local"] の値を取り扱う際に利用 <input type="datetime-local" value="2025-09-01T12:34:56" /> //

    "2025-09-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-09-01T12:34:56" export function zdtToInputDateTime(t: Temporal.ZonedDateTime) { return t.toPlainDateTime().toString({ smallestUnit: "second" }); } フロントエンドカンファレンス北海道2025 LT @8beeeaaat 11
  7. 前後比較に一癖ある 基本的にはstatic methodの compare() を利用する // 第1引数が第2引数に対し、過去: -1, 一致: 0,

    未来: 1 const result = Temporal.ZonedDateTime.compare(a, b); const aIsBefore = result < 0; いまいち直感的に扱えない date-fns Likeなユーティリティを自作 export const isBefore = (a: Temporal.ZonedDateTime, b: Temporal.ZonedDateTime) => { return Temporal.ZonedDateTime.compare(a, b) < 0; } フロントエンドカンファレンス北海道2025 LT @8beeeaaat 12
  8. 独自インスタンスメソッド tzd.isBefore(other) 的に扱えるインスタンスメソッドを生やす。 一致を検証する tzd.equals(other) は存在する declare module 'temporal-polyfill' {

    namespace Temporal { interface ZonedDateTime { isBefore(target: Temporal.ZonedDateTime): boolean; } } } Temporal.ZonedDateTime.prototype.isBefore = function ( this: Temporal.ZonedDateTime, target: Temporal.ZonedDateTime ) { return Temporal.ZonedDateTime.compare(this, target) < 0; } aZdt.isBefore(bZdt) => true フロントエンドカンファレンス北海道2025 LT @8beeeaaat 13
  9. 実践: Form + Zodスキーマ連携 開始 / 終了時刻の前後関係を検証したい import { 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"], }) フロントエンドカンファレンス北海道2025 LT @8beeeaaat 14
  10. 実践: BE / FE間ではRFC3339フォーマットを使う func getEvent(w http.ResponseWriter, r *http.Request) {

    location, _ := time.LoadLocation("Asia/Tokyo") event := struct { ID string `json:"id"` Name string `json:"name"` StartsAt time.Time `json:"starts_at"` CreatedAt time.Time `json:"created_at"` }{ ID: "1", Name: "フロントエンドカンファレンス北海道2025", StartsAt: time.Date(2025, 9, 6, 10, 0, 0, 0, location), CreatedAt: time.Now().UTC(), //ナノ秒精度のマシンUTC日時 } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(event) } フロントエンドカンファレンス北海道2025 LT @8beeeaaat 15
  11. APIレスポンス 時刻は最大ナノ秒精度のRFC3339フォーマット文字列に置き換えられる { id: '1', name: 'フロントエンドカンファレンス北海道2025', starts_at: '2025-09-06T10:00:00+09:00', //

    指定locationのOffsetDateTimeに変換される created_at: '2025-09-05T13:40:39.123456789Z' // 最大ナノ秒精度のUTC日時に変換される } フロントエンドでもナノ秒精度のまま扱える const event = await fetch("/api/events/1").then(r => r.json()); const startsAt = rfc3339ToZdt(event.starts_at); => Temporal.ZonedDateTime 2025-09-06T10:00:00+09:00[Asia/Tokyo] const createdAt = rfc3339ToZdt(event.created_at); => Temporal.ZonedDateTime 2025-09-05T22:40:39.123456789+09:00[Asia/Tokyo] フロントエンドカンファレンス北海道2025 LT @8beeeaaat 16
  12. Temporalを触って備える 標準化間近: https://github.com/tc39/proposal-temporal/milestones TC39 Stage 3 (仕様策定) → Stage 4

    (標準化) への最終段階 ブラウザ実装: Firefoxは一足お先に実装済み V8はRustライブラリとの統合を始めている。JavaScriptCoreも進捗中 らしい まだpolyfillのお世話になりそう フロントエンドカンファレンス北海道2025 LT @8beeeaaat 17
  13. 参考資料 TC39 Temporal Proposal Temporal - JavaScript | MDN @sajikix

    Temporalの近況(主にScopeを狭める話) thx :) フロントエンドカンファレンス北海道2025 LT @8beeeaaat 19