- JavaScript
- 技術 💻
JavaScript Temporal 使い方|Dateからの移行早見表と実行結果
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期連続の黒字決算。