Temporalとは、JavaScriptの Date に代わる新しい標準の日時APIです。「日付だけ」「時刻だけ」「タイムゾーン付き日時」「経過時間」などを別々の型で扱い、値は変更不可(イミュータブル)なので、月末計算やタイムゾーン変換を安全に書けます。

この記事では、「Dateでやっていたこと → Temporalでの書き方」の対照表 と、そのまま貼って動くコードを用意しました。すべてのコードは Node.js 26.10.0 のネイティブTemporal と、Node.js 22 + @js-temporal/polyfill 0.5.1 の両方で実行し、実際の出力を載せています。ネイティブとpolyfillでエラー文が違う点や、ブラウザ・ランタイムの対応バージョン(MDNのbrowser-compat-data 8.1.3で確認)もまとめました。


Date → Temporal 早見表

やりたいこと Date での書き方 Temporal での書き方
今日の日付 new Date() から年月日を取り出す Temporal.Now.plainDateISO('Asia/Tokyo')
現在の日時(タイムゾーン付き) new Date() Temporal.Now.zonedDateTimeISO('Asia/Tokyo')
現在の瞬間(UTC) Date.now() Temporal.Now.instant()
文字列から日付 new Date('2026-09-28')(UTC扱い) Temporal.PlainDate.from('2026-09-28')
1か月後 d.setMonth(d.getMonth() + 1)(1/31→3/3) date.add({ months: 1 })(1/31→2/28)
n日後 d.setDate(d.getDate() + n) date.add({ days: n })
月末日 new Date(y, m + 1, 0) date.with({ day: date.daysInMonth })
2日間の差 (b - a) / 86400000 a.until(b) / b.since(a)
表示用の書式 toLocaleString('ja-JP') toLocaleString('ja-JP')(下記の注意あり)
ISO文字列に toISOString()(常にUTC) toString()(型に応じた形式)
タイムゾーン変換 標準では困難 zdt.withTimeZone('America/New_York')
大小比較 a < b Temporal.PlainDate.compare(a, b)
等しいか a.getTime() === b.getTime() a.equals(b)
並べ替え sort((a, b) => a - b) sort(Temporal.PlainDate.compare)
曜日 getDay()(日曜=0) dayOfWeek(月曜=1〜日曜=7)
経過時間を表す ミリ秒の数値 Temporal.Duration
Dateと相互変換 ― Temporal.Instant.fromEpochMilliseconds(d.getTime()) / new Date(i.epochMilliseconds)

型の使い分け早見表

型 持っている情報 文字列の例 使う場面
PlainDate 年月日 2026-09-28 誕生日・締め切り日・予約日
PlainTime 時分秒 09:30:00 営業開始時刻・アラーム時刻
PlainDateTime 年月日+時分秒(TZなし) 2026-09-28T09:30:00 入力フォームの値、TZ未確定の予定
ZonedDateTime 日時+タイムゾーン 2026-09-28T10:00:00+09:00[Asia/Tokyo] 会議・配信予定などの「ある地域の日時」
Instant UTC上の一瞬 2026-09-28T01:00:00Z ログ・DB保存・APIのタイムスタンプ
Duration 期間 PT2H30M 所要時間・残り日数
PlainYearMonth / PlainMonthDay 年月 / 月日 2026-09 / 09-28 月次集計 / 記念日

迷ったら「タイムゾーンが必要か」で決めます。必要なら ZonedDateTime(保存は Instant)、不要なら Plain〜 系です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript Temporal 使い方|Dateからの移行早見表と実行結果」


Temporalの対応状況(ブラウザ・ランタイム)

MDNの browser-compat-data(npm @mdn/browser-compat-data 8.1.3)の javascript.builtins.Temporal を読み取った結果です。

環境 対応バージョン
Chrome / Edge 144
Chrome for Android 144
Opera 128
Firefox / Firefox for Android 139
Safari Technology Preview のみ
Safari on iOS 未対応
Node.js 26.0.0
Deno 2.7
Bun 1.4.0

Safari(iOS含む)がまだ正式対応していないため、公開サイトでは polyfill の併用が現実的 です。この検証環境でも、Node 22 と Playwright の Chromium 141 では typeof Temporal が undefined でした。

polyfill の入れ方(2種類)

パッケージ 検証したバージョン 特徴
@js-temporal/polyfill 0.5.1 仕様策定チームが関わる実装。import { Temporal } で使う
temporal-polyfill 1.0.5 import 'temporal-polyfill/global' でグローバルに追加できる
npm i @js-temporal/polyfill
// ネイティブがあればそれを使い、無ければ polyfill を読み込む(ESM)
if (typeof globalThis.Temporal === 'undefined') {
  const m = await import('@js-temporal/polyfill');
  globalThis.Temporal = m.Temporal;
}
console.log(Temporal.PlainDate.from('2026-01-31').add({ months: 1 }).toString());
// temporal-polyfill をグローバルに入れる書き方
import 'temporal-polyfill/global';
console.log(typeof Temporal, Temporal.PlainDate.from('2026-01-31').add({ months: 1 }).toString());
object 2026-02-28

TypeScriptで型が見つからない場合は、使っている lib 設定と polyfill の型定義を確認してください。


検証環境

項目 バージョン
ネイティブ Node.js 26.10.0(npx -y node@26 t.mjs)
polyfill Node.js 22.22.2 + @js-temporal/polyfill 0.5.1
タイムゾーン TZ=Asia/Tokyo を指定して実行

以下の「実行結果」は、特に断りがなければ ネイティブとpolyfillで同じ出力 でした。違いがあったものは両方を載せています。


今日の日付・現在時刻を取得する

console.log(Temporal.Now.plainDateISO('Asia/Tokyo').toString());
console.log(Temporal.Now.zonedDateTimeISO('Asia/Tokyo').round('second').toString());
console.log(Temporal.Now.instant().round('second').toString());
console.log(Temporal.Now.timeZoneId());
2026-09-29
2026-09-29T12:40:10+09:00[Asia/Tokyo]
2026-09-29T03:40:10Z
Asia/Tokyo

タイムゾーンIDを引数で明示すると、実行するサーバーの設定に左右されません。省略すると Temporal.Now.timeZoneId()(実行環境のタイムゾーン)が使われます。


1か月後を計算する(Dateの「1/31+1か月」問題)

// Date:1月31日の1か月後
const d = new Date(2026, 0, 31);
d.setMonth(d.getMonth() + 1);
console.log(d.toLocaleDateString('ja-JP'));

// Temporal
const p = Temporal.PlainDate.from('2026-01-31');
console.log(p.add({ months: 1 }).toString());
console.log(Temporal.PlainDate.from('2028-01-31').add({ months: 1 }).toString()); // うるう年
2026/3/3
2026-02-28
2028-02-29

Dateは「2月31日」を作ってから3月3日に繰り越しますが、Temporalは既定で月末に丸めます(overflow: 'constrain')。丸めずにエラーにしたいときは overflow: 'reject' を指定します。

try {
  p.add({ months: 1 }, { overflow: 'reject' });
} catch (e) {
  console.log(`${e.name}: ${e.message}`);
}
実行環境 出力
Node 26 ネイティブ RangeError: Temporal error: not a valid ISO date.
@js-temporal/polyfill RangeError: value out of range: 1 <= 31 <= 28

エラーの種類(RangeError)は同じですが、メッセージ文は実装ごとに違います。エラー処理では e.message ではなく e instanceof RangeError で判定すると、どちらでも動きます。

月末日・月初日

const day = Temporal.PlainDate.from('2026-02-10');
console.log(day.with({ day: day.daysInMonth }).toString()); // 月末
console.log(day.with({ day: 1 }).toString());               // 月初
console.log(day.daysInMonth, day.inLeapYear);
2026-02-28
2026-02-01
28 false

daysInMonth と with を組み合わせると、月末日の計算に new Date(y, m + 1, 0) のような工夫が要りません。


日付を表示用の書式にする

const zdt = Temporal.ZonedDateTime.from('2026-09-28T14:05:09+09:00[Asia/Tokyo]');
console.log(zdt.toString());
console.log(zdt.toPlainDate().toString());
console.log(zdt.toLocaleString('ja-JP'));
console.log(zdt.toLocaleString('ja-JP', { dateStyle: 'full', timeStyle: 'short' }));
console.log(zdt.toPlainDate().toLocaleString('ja-JP-u-ca-japanese',
  { era: 'long', year: 'numeric', month: 'long', day: 'numeric' }));

const pad = n => String(n).padStart(2, '0');
console.log(`${zdt.year}/${pad(zdt.month)}/${pad(zdt.day)} ${pad(zdt.hour)}:${pad(zdt.minute)}`);
console.log(zdt.dayOfWeek, zdt.weekOfYear);
処理 Node 26 ネイティブ @js-temporal/polyfill
toString() 2026-09-28T14:05:09+09:00[Asia/Tokyo] 同じ
toPlainDate() 2026-09-28 同じ
toLocaleString('ja-JP') 2026/9/28 14:05:09 JST 同じ
dateStyle: 'full' 2026/9/28月曜日 14:05 2026年9月28日月曜日 14:05
和暦 令和8/9/28 令和8/9/28
自作書式 2026/09/28 14:05 同じ
dayOfWeek / weekOfYear 1 / 40 同じ

dateStyle: 'full' の結果がネイティブとpolyfillで違いました。同じNode 26で Date を同じ指定で書式化すると 2026年9月28日月曜日 14:05 になり、Intl.DateTimeFormat に Temporal オブジェクトを渡した場合も 2026/9/28月曜日 でした。

// Node 26.10.0 で確認した回避策:表示の直前だけ Date に変換する
const pd = Temporal.PlainDate.from('2026-09-28');
const ms = pd.toZonedDateTime('Asia/Tokyo').epochMilliseconds;
console.log(new Date(ms).toLocaleString('ja-JP', { dateStyle: 'full' }));
2026年9月28日月曜日

画面に出す書式が重要な場合は、「自作書式(year/month/day を組み立てる)」か「表示直前にDateへ変換」 にしておくと、実装差の影響を受けません。実装の更新で変わる可能性があるため、利用する環境で一度確認してください。


2つの日付の差(until / since)

const a = Temporal.PlainDate.from('2026-09-28');
const b = Temporal.PlainDate.from('2026-12-25');
console.log(a.until(b).toString());
console.log(a.until(b, { largestUnit: 'month' }).toString());
console.log(b.since(a).days);
console.log(a.until(b).total({ unit: 'week', relativeTo: a }).toFixed(2));

const t1 = Temporal.PlainTime.from('09:30');
const t2 = Temporal.PlainTime.from('18:15');
console.log(t1.until(t2).toString());

// Date の場合
console.log((new Date(2026, 11, 25) - new Date(2026, 8, 28)) / 86400000);
P88D
P2M27D
88
12.57
PT8H45M
88
書き方 意味
a.until(b) aからbまで(bが後なら正)
b.since(a) aからbまでの経過(同じ値)
{ largestUnit: 'month' } 「2か月27日」のように月単位で表す
.total({ unit: 'week', relativeTo: a }) 週換算の小数

P88D は ISO 8601 の期間表記で「88日」 です。P2M27D は2か月27日、PT8H45M は8時間45分を表します。


タイムゾーンを変換する

const tokyo = Temporal.ZonedDateTime.from('2026-09-28T10:00[Asia/Tokyo]');
console.log(tokyo.toString());
console.log(tokyo.withTimeZone('America/New_York').toString());
console.log(tokyo.withTimeZone('Europe/London').toString());
console.log(tokyo.toInstant().toString());
2026-09-28T10:00:00+09:00[Asia/Tokyo]
2026-09-27T21:00:00-04:00[America/New_York]
2026-09-28T02:00:00+01:00[Europe/London]
2026-09-28T01:00:00Z

withTimeZone は同じ瞬間を別の地域の時計で表したもの です。9月はニューヨーク・ロンドンとも夏時間のため、オフセットが -04:00 / +01:00 になっています。


日時を比較・並べ替える

const x = Temporal.PlainDate.from('2026-10-01');
const y = Temporal.PlainDate.from('2026-09-30');
console.log(Temporal.PlainDate.compare(x, y));
console.log(x.equals(y));
console.log(['2026-10-01', '2026-09-30', '2026-01-15']
  .map(s => Temporal.PlainDate.from(s))
  .sort(Temporal.PlainDate.compare)
  .join(', '));
try { console.log(x > y); } catch (e) { console.log(`${e.name}: ${e.message}`); }
1
false
2026-01-15, 2026-09-30, 2026-10-01

x > y は比較にならず、TypeErrorになります。

実行環境 x > y の結果
Node 26 ネイティブ TypeError: Do not use Temporal.PlainDate.prototype.valueOf; use Temporal.PlainDate.prototype.compare for comparison.
@js-temporal/polyfill TypeError: Do not use built-in arithmetic operators with Temporal objects. When comparing, use Temporal.PlainDate.compare(obj1, obj2), not obj1 > obj2. …

Dateで a < b と書いていた箇所は、Temporal.〇〇.compare(a, b) < 0 に置き換え ます。compare は -1 / 0 / 1 を返すので、そのまま sort に渡せます。


ISO文字列を読み込む(解析)

console.log(Temporal.Instant.from('2026-09-28T01:00:00Z').toString());
console.log(Temporal.PlainDate.from('2026-09-28').toString());
console.log(Temporal.ZonedDateTime.from('2026-09-28T10:00[Asia/Tokyo]').toString());
try { Temporal.Instant.from('2026-09-28T10:00:00'); } catch (e) { console.log(`${e.name}: ${e.message}`); }
try { Temporal.PlainDate.from('2026/09/28'); } catch (e) { console.log(`${e.name}: ${e.message}`); }
2026-09-28T01:00:00Z
2026-09-28
2026-09-28T10:00:00+09:00[Asia/Tokyo]
入力 Node 26 ネイティブ @js-temporal/polyfill
Instant.from('2026-09-28T10:00:00')(オフセットなし) RangeError: Temporal error: Required fields missing from Instant string. RangeError: Temporal.Instant requires a time zone offset
PlainDate.from('2026/09/28')(スラッシュ区切り) RangeError: Temporal error: Invalid character while parsing month value. RangeError: invalid RFC 9557 string: 2026/09/28

Temporalは RFC 9557(ISO 8601の拡張)形式だけを受け付けます。スラッシュ区切りなどの入力は、先に - 区切りへ整えるか、PlainDate.from({ year, month, day }) のようにオブジェクトで渡します。

Dateの「日付だけの文字列はUTC」との違い

console.log(new Date('2026-09-28').toString());
console.log(new Date('2026-09-28T00:00').toString());
Mon Sep 28 2026 09:00:00 GMT+0900 (Japan Standard Time)
Mon Sep 28 2026 00:00:00 GMT+0900 (Japan Standard Time)

Dateは日付だけの文字列をUTCとして、時刻付きの文字列をローカル時刻として解釈するため、日本時間では9時間ずれます。Temporal.PlainDate.from('2026-09-28') はタイムゾーンを持たないので、このずれは起きません。


夏時間(DST)をまたぐ計算

const ny = Temporal.ZonedDateTime.from('2026-03-07T12:00[America/New_York]');
console.log(ny.add({ days: 1 }).toString());
console.log(ny.add({ hours: 24 }).toString());
console.log(ny.add({ days: 1 }).hoursInDay);
// 夏時間開始で存在しない 02:30
console.log(Temporal.ZonedDateTime.from('2026-03-08T02:30[America/New_York]').toString());
try {
  Temporal.ZonedDateTime.from(
    { timeZone: 'America/New_York', year: 2026, month: 3, day: 8, hour: 2, minute: 30 },
    { disambiguation: 'reject' });
} catch (e) { console.log(`${e.name}: ${e.message}`); }
2026-03-08T12:00:00-04:00[America/New_York]
2026-03-08T13:00:00-04:00[America/New_York]
23
2026-03-08T03:30:00-04:00[America/New_York]

disambiguation: 'reject' の結果は、ネイティブが RangeError: Temporal error: Rejecting ambiguous time zones.、polyfill が RangeError: multiple instants found でした。

書き方 結果 使う場面
add({ days: 1 }) 翌日の 同じ時計の時刻(12:00) 「毎日12時」の予定
add({ hours: 24 }) 実際に24時間後(13:00) タイマー・有効期限
hoursInDay その日の長さ(切り替え日は23) 日次集計の確認
存在しない時刻(既定) 1時間後ろへずらす(03:30) 入力値の補正
disambiguation: 'reject' RangeError 入力ミスとして扱いたいとき

「1日後」と「24時間後」が別物として書き分けられる のがTemporalの大きな利点です。


Date と相互に変換する

const date = new Date('2026-09-28T01:00:00Z');
const inst = Temporal.Instant.fromEpochMilliseconds(date.getTime());
console.log(inst.toString());
console.log(inst.toZonedDateTimeISO('Asia/Tokyo').toString());
console.log(new Date(inst.epochMilliseconds).toISOString());
console.log(typeof Date.prototype.toTemporalInstant);
2026-09-28T01:00:00Z
2026-09-28T10:00:00+09:00[Asia/Tokyo]
2026-09-28T01:00:00.000Z
function      ← Node 26 ネイティブ
undefined     ← @js-temporal/polyfill(Dateの拡張はしない)

date.toTemporalInstant() はネイティブ環境にはありますが、polyfill では使えませんでした。両方で動かすなら Temporal.Instant.fromEpochMilliseconds(date.getTime()) が確実です。


期間(Duration)を扱う

const dur = Temporal.Duration.from({ hours: 1, minutes: 90 });
console.log(dur.toString());
console.log(dur.round({ largestUnit: 'hour' }).toString());
console.log(dur.total('minute'));
console.log(Temporal.Duration.from('PT2H30M').minutes);
console.log(JSON.stringify({
  due: Temporal.PlainDate.from('2026-10-01'),
  at: Temporal.ZonedDateTime.from('2026-09-28T10:00[Asia/Tokyo]'),
}));
PT1H90M
PT2H30M
150
30
{"due":"2026-10-01","at":"2026-09-28T10:00:00+09:00[Asia/Tokyo]"}

Durationは自動で繰り上げません(PT1H90M のまま)。round({ largestUnit: 'hour' }) で PT2H30M に整えます。JSONにするとISO文字列になる ので、APIで受け取った側は Temporal.PlainDate.from() などで戻せます。


値が変わらない(イミュータブル)ことの確認

const base = Temporal.PlainDate.from('2026-09-28');
const next = base.add({ days: 1 });
console.log(`${base} / ${next}`);

const dd = new Date(2026, 8, 28);
const dd2 = dd;               // 同じオブジェクトを参照
dd2.setDate(dd2.getDate() + 1);
console.log(dd.toLocaleDateString('ja-JP'));
2026-09-28 / 2026-09-29
2026/9/29

Temporalの add は新しい値を返し、元の値はそのまま です。Dateは setDate で元のオブジェクトが書き換わるため、共有していた変数 dd も変わりました。


移行時のチェックリスト

チェック項目 対応
対象ブラウザに Safari が含まれる polyfill を読み込む(機能検出で切り替え)
a < b で日付を比較している Temporal.〇〇.compare(a, b) に置き換え
setMonth で月を加算している add({ months })。月末の扱いを overflow で明示
日付だけの文字列を new Date() に渡している Temporal.PlainDate.from() に置き換え
DBやAPIに保存する日時 Instant(UTC)で保存し、表示時に ZonedDateTime へ
エラーメッセージで分岐している instanceof RangeError で判定(実装で文言が違う)
画面表示の書式が重要 自作書式、または表示直前にDateへ変換して確認
ライブラリ(日付系)と併用している 境界で epochMilliseconds を使って受け渡し

よくある質問

Q. Temporal はもう使ってよいですか? A. 標準化は完了しており(MDNのデータでは非実験的・標準トラック)、Chrome/Edge 144、Firefox 139、Node.js 26、Deno 2.7、Bun 1.4.0 で使えます。Safari は Technology Preview のみなので、公開サイトでは polyfill と併用するのが安心です。

Q. @js-temporal/polyfill と temporal-polyfill はどちらを選べばよいですか? A. どちらもこの記事のコードで動作しました。import { Temporal } で明示的に使いたいなら前者、import 'temporal-polyfill/global' でグローバルに生やしたいなら後者が書きやすいです。バンドルサイズを重視する場合は、それぞれのnpmページで最新情報を確認してください。

Q. PlainDateTime と ZonedDateTime はどちらを使えばよいですか? A. 「どの地域の時刻か」が決まっているなら ZonedDateTime です。夏時間や他地域への変換が正しく計算されます。フォーム入力のように地域がまだ決まっていない値だけ PlainDateTime で持ち、確定時に toZonedDateTime('Asia/Tokyo') で変換します。

Q. Temporal オブジェクトを文字列連結したらエラーになりました。 A. 大小比較や + による数値変換は TypeError になるよう設計されています。文字列にするときは `${obj}` か obj.toString() を使います。


まとめ

  • Temporal は Date に代わる標準の日時APIで、型ごとに役割が分かれ、値は変更不可 です。
  • 「1/31+1か月」は Date だと 3/3、Temporal だと 2/28 と実行で確認できました。
  • 差分は until / since、比較は compare、タイムゾーン変換は withTimeZone で書けます。
  • 夏時間をまたぐと add({ days: 1 }) と add({ hours: 24 }) の結果が1時間違い、用途で書き分けられます。
  • ネイティブと polyfill ではエラー文や一部の書式出力が違う ため、instanceof での判定や表示前の確認が有効です。
  • Chrome/Edge 144・Firefox 139・Node.js 26 で対応、Safari は準備中 なので、当面は polyfill と併用します。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript Temporal 使い方|Dateからの移行早見表と実行結果」

株式会社RJCでは、Temporalのような新しい標準APIを実際に動かして確かめ、プロジェクトの中で仲間と知見を共有しています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?