JavaScript Temporal API完全解説:Dateの地獄から解放される日がやってきた
JavaScriptで日付処理を書いたことがある方なら、あの苦労をよくご存知でしょう。
UTCとローカル時間の混乱。月が0始まり(1月が0って何ですか…)。どこからでも変更できてしまうミュータブルなDateオブジェクト。タイムゾーン変換の悪夢。そしてmoment.jsやdate-fnsなしでは基本的な処理すらできない現実。
Dateは1995年から壊れています。もう30年近く、この問題と付き合ってきました。でも、ついに救世主が登場しました!
Temporal APIは2017年から開発が進められてきた新しい日付/時刻APIで、TC39のStage 3に到達しました。Chrome、Firefox、Safariすべてで実装が進んでおり、2025年半ばにはどの環境でも使えるようになる見込みです。
この記事では、Temporalについて詳しく解説していきます。なぜ必要なのか、どう使うのか、moment.jsからどう移行するのかまで、しっかりカバーします。
なぜDateはこんなに使いづらいのか?
Temporalに入る前に、なぜ代替が必要だったのかを整理しておきましょう。Dateは単に不便なだけでなく、本番環境でバグを引き起こす構造的な問題を抱えています。
問題1:ミュータブル(変更可能)である
```javascript
const meeting = new Date('2025-01-15T10:00:00');
scheduleMeeting(meeting);
// どこか別の場所で...
meeting.setHours(meeting.getHours() + 2);
// あれ?元の値が変わってしまった!
```
一度渡したDateオブジェクトは、誰かに変更されている可能性があります。信用できません。
問題2:月が0始まり(定番の罠)
```javascript
// この日付、何月でしょう?
const date = new Date(2025, 1, 14);
console.log(date.toISOString()); // 2025-02-14 ← 2月です!
// 1月を作るには0を使う
const january = new Date(2025, 0, 15);
```
12月25日を作ろうとしてnew Date(2025, 12, 25)と書いて、2026年1月25日になった経験、ありますよね?
問題3:タイムゾーンの混乱
```javascript
const date = new Date('2025-01-15T10:00:00');
console.log(date.getHours()); // 実行環境のタイムゾーン次第
```
絶対時刻を保持しながらローカル時間で表示するため、「東京の午前10時」を表現する手段がありません。
問題4:日付計算がおかしい
```javascript
// 1月31日 + 1ヶ月 = ?
const jan31 = new Date(2025, 0, 31);
jan31.setMonth(jan31.getMonth() + 1);
console.log(jan31.toDateString()); // "Mon Mar 03 2025" ← えっ?
```
2月は31日がないので、3月3日にオーバーフローしてしまいます。😱
Temporal登場:ゼロから設計された日付API
Temporalはこれらの問題をすべて解決するために、一から設計されました。
設計原則
- イミュータブル:一度作成したら変更不可。操作は新しいオブジェクトを返す
- 明確な型:用途ごとに異なる型(日付、時刻、タイムゾーン、期間)
- 予測可能:月は1始まり。パースは厳密
- タイムゾーンネイティブ:IANAタイムゾーンデータベースを内蔵
- 人間に優しい:期待通りに動作するAPI
型の種類
```javascript
Temporal.PlainDate // 日付のみ (2025-01-15)
Temporal.PlainTime // 時刻のみ (10:30:00)
Temporal.PlainDateTime // 日付+時刻 (2025-01-15T10:30:00)
Temporal.ZonedDateTime // 日付+時刻+TZ (2025-01-15T10:30:00[Asia/Tokyo])
Temporal.Instant // 絶対時刻(Dateの代替、イミュータブル)
Temporal.Duration // 期間(2時間30分)
Temporal.PlainYearMonth // 年月のみ (2025-01)
Temporal.PlainMonthDay // 月日のみ (01-15、誕生日など)
```
型名を見るだけで何を表しているか分かります。PlainDateなら「タイムゾーンのない純粋な日付」だとすぐ理解できますね。
Part 1:日付を扱う(Temporal.PlainDate)
タイムゾーンなしで純粋に日付だけを扱いたいときに使います。誕生日、祝日、締め切りなどに最適です。
作成する
```javascript
// オブジェクトから(月は1始まり!)
const date1 = Temporal.PlainDate.from({ year: 2025, month: 1, day: 15 });
console.log(date1.toString()); // "2025-01-15"
// 文字列から
const date2 = Temporal.PlainDate.from('2025-01-15');
// 今日の日付
const today = Temporal.Now.plainDateISO();
```
情報を取得する
```javascript
const date = Temporal.PlainDate.from('2025-06-15');
console.log(date.year); // 2025
console.log(date.month); // 6(ちゃんと6月!)
console.log(date.day); // 15
console.log(date.dayOfWeek); // 7(日曜日。月曜日=1)
console.log(date.daysInMonth); // 30
```
日付計算(やっとまともに動く!)
```javascript
const date = Temporal.PlainDate.from('2025-01-31');
// 1ヶ月足す - Temporalがちゃんと処理してくれる
const nextMonth = date.add({ months: 1 });
console.log(nextMonth.toString()); // "2025-02-28" ← 2月末日に調整
// 2週間と3日足す
const later = date.add({ weeks: 2, days: 3 });
console.log(later.toString()); // "2025-02-17"
// チェーンもきれいに書ける
const result = date
.add({ months: 6 })
.add({ days: 15 })
.subtract({ weeks: 1 });
```
比較と差分計算
```javascript
const date1 = Temporal.PlainDate.from('2025-01-15');
const date2 = Temporal.PlainDate.from('2025-03-20');
const diff = date2.since(date1);
console.log(diff.days); // 64
const diffMonths = date2.since(date1, { largestUnit: 'month' });
console.log(diffMonths.toString()); // "P2M5D"(2ヶ月5日)
```
Part 2:時刻を扱う(Temporal.PlainTime)
時計の表示時刻を表します。日付やタイムゾーンは含みません。
```javascript
const time = Temporal.PlainTime.from({ hour: 10, minute: 30 });
console.log(time.toString()); // "10:30:00"
// 時刻計算
const later = time.add({ hours: 2, minutes: 45 });
console.log(later.toString()); // "13:15:00"
// 日付をまたぐとラップアラウンド
const pastMidnight = time.add({ hours: 16 });
console.log(pastMidnight.toString()); // "02:30:00"
```
Part 3:日付+時刻(Temporal.PlainDateTime)
日付と時刻の両方が必要だけど、タイムゾーンは不要な場合に使います。
```javascript
const dt = Temporal.PlainDateTime.from({
year: 2025, month: 1, day: 15,
hour: 10, minute: 30
});
console.log(dt.toString()); // "2025-01-15T10:30:00"
// 日付と時刻を別々に作って合成
const date = Temporal.PlainDate.from('2025-01-15');
const time = Temporal.PlainTime.from('10:30:00');
const combined = date.toPlainDateTime(time);
```
Part 4:本命!タイムゾーン(Temporal.ZonedDateTime)
ここがTemporalの真骨頂です。ZonedDateTimeは「東京の午前10時」「ニューヨークの午後3時」といった表現を正確に扱え、サマータイムも自動処理してくれます。
作成する
```javascript
const zdt = Temporal.ZonedDateTime.from({
year: 2025, month: 1, day: 15,
hour: 10, minute: 30,
timeZone: 'Asia/Tokyo'
});
console.log(zdt.toString());
// "2025-01-15T10:30:00+09:00[Asia/Tokyo]"
// 別のタイムゾーンの現在時刻
const nyNow = Temporal.Now.zonedDateTimeISO('America/New_York');
```
タイムゾーン変換
```javascript
const tokyoTime = Temporal.ZonedDateTime.from(
'2025-01-15T10:30:00[Asia/Tokyo]'
);
// ニューヨーク時間に変換
const nyTime = tokyoTime.withTimeZone('America/New_York');
console.log(nyTime.toString());
// "2025-01-14T20:30:00-05:00[America/New_York]"
// UTCに変換
const utc = tokyoTime.withTimeZone('UTC');
```
サマータイムの処理
```javascript
// アメリカでは3月の第2日曜日に時計が1時間進む
const beforeDST = Temporal.ZonedDateTime.from(
'2025-03-09T01:30:00[America/New_York]'
);
const afterDST = beforeDST.add({ hours: 1 });
console.log(afterDST.toString());
// "2025-03-09T03:30:00-04:00[America/New_York]"
// 1:30 + 1時間 = 3:30(2:30は存在しない!)
```
Part 5:期間を扱う(Temporal.Duration)
```javascript
const duration = Temporal.Duration.from({ hours: 2, minutes: 30 });
console.log(duration.toString()); // "PT2H30M"
// 期間の足し算
const d1 = Temporal.Duration.from({ hours: 1, minutes: 30 });
const d2 = Temporal.Duration.from({ hours: 2, minutes: 45 });
console.log(d1.add(d2).toString()); // "PT4H15M"
// 150分を時間に変換
const mins = Temporal.Duration.from({ minutes: 150 });
const balanced = mins.round({ largestUnit: 'hour' });
console.log(balanced.toString()); // "PT2H30M"
```
実用例
```javascript
// 年齢計算
const birthdate = Temporal.PlainDate.from('1990-05-15');
const today = Temporal.Now.plainDateISO();
const age = today.since(birthdate, { largestUnit: 'year' });
console.log(`${age.years}歳 ${age.months}ヶ月`);
// カウントダウン
const deadline = Temporal.PlainDate.from('2025-12-31');
const remaining = deadline.since(today);
console.log(`あと${remaining.days}日`);
```
Part 6:実践的な活用例
グローバルミーティングの調整
```javascript
function scheduleMeeting(dateTime, hostTz, attendeeTzs) {
const meeting = Temporal.ZonedDateTime.from(`${dateTime}[${hostTz}]`);
const schedule = new Map();
schedule.set(hostTz, meeting.toString());
for (const tz of attendeeTzs) {
schedule.set(tz, meeting.withTimeZone(tz).toString());
}
return schedule;
}
// 東京で午前9時にミーティングを設定したら
// ニューヨークとロンドンは何時?
scheduleMeeting('2025-01-20T09:00:00', 'Asia/Tokyo',
['America/New_York', 'Europe/London']);
```
営業日計算
```javascript
function addBusinessDays(start, days) {
let current = start;
let remaining = days;
while (remaining > 0) {
current = current.add({ days: 1 });
if (current.dayOfWeek < 6) { // 土日をスキップ
remaining--;
}
}
return current;
}
```
Part 7:moment.jsからの移行
コード比較
```javascript
// moment.js
const m = moment('2025-01-15');
m.add(1, 'month'); // 注意!元のオブジェクトが変更される
// Temporal
const t = Temporal.PlainDate.from('2025-01-15');
const next = t.add({ months: 1 }); // 元は変更されない
// moment-timezone
moment.tz('2025-01-15 10:30', 'Asia/Tokyo');
// Temporal
Temporal.ZonedDateTime.from('2025-01-15T10:30:00[Asia/Tokyo]');
```
移行戦略
- 新規コード → Temporalで書く
- アダプター関数 → 既存コードとの橋渡し
- 段階的に置き換え → リスクの高いコードから
- 依存関係を削除 → 全て移行したらmoment.jsをアンインストール
```javascript
// 変換用ユーティリティ
function dateToTemporal(date) {
return Temporal.Instant.fromEpochMilliseconds(date.getTime());
}
function temporalToDate(instant) {
return new Date(instant.epochMilliseconds);
}
```
Part 8:今すぐ使うには?
2024年末時点では、ブラウザにデフォルトでは入っていません。
| 環境 | 状況 |
|---|---|
| Chrome | フラグで利用可能 |
| Firefox | 開発中 |
| Safari | 開発中 |
| Node.js | フラグで利用可能 |
Polyfillで今すぐ使える
```bash
npm install @js-temporal/polyfill
```
```javascript
import { Temporal } from '@js-temporal/polyfill';
const today = Temporal.Now.plainDateISO();
```
ネイティブサポートの検出
```javascript
async function getTemporal() {
if (typeof globalThis.Temporal !== 'undefined') {
return globalThis.Temporal;
}
const { Temporal } = await import('@js-temporal/polyfill');
return Temporal;
}
```
まとめ
Temporal APIは、JavaScript史上最も重要な日付処理の改善です。
なぜ素晴らしいのか:
- イミュータブル → 予期しない変更がなくなる
- 型が明確 → PlainDate、ZonedDateTimeなど一目で分かる
- 直感的 → 月が1始まり、メソッド名も分かりやすい
- タイムゾーンがファーストクラス → サマータイムも含めて自動処理
- 日付計算が正しい → やっと期待通りに動く
次のステップ:
- Polyfillで今日から試してみる
- 新規プロジェクトにはTemporalを使う
- moment.jsからの脱却を計画する
- ブラウザの実装状況をウォッチする
Dateとの戦いは、もうすぐ終わります。Temporal時代の幕開けです!🚀
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう