Back

JavaScript Temporal API完全解説:Dateの地獄から解放される日がやってきた

JavaScriptで日付処理を書いたことがある方なら、あの苦労をよくご存知でしょう。

UTCとローカル時間の混乱。月が0始まり(1月が0って何ですか…)。どこからでも変更できてしまうミュータブルなDateオブジェクト。タイムゾーン変換の悪夢。そしてmoment.jsdate-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. イミュータブル:一度作成したら変更不可。操作は新しいオブジェクトを返す
  2. 明確な型:用途ごとに異なる型(日付、時刻、タイムゾーン、期間)
  3. 予測可能:月は1始まり。パースは厳密
  4. タイムゾーンネイティブ:IANAタイムゾーンデータベースを内蔵
  5. 人間に優しい:期待通りに動作する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]');
```

移行戦略

  1. 新規コード → Temporalで書く
  2. アダプター関数 → 既存コードとの橋渡し
  3. 段階的に置き換え → リスクの高いコードから
  4. 依存関係を削除 → 全て移行したら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史上最も重要な日付処理の改善です。

なぜ素晴らしいのか:

  1. イミュータブル → 予期しない変更がなくなる
  2. 型が明確 → PlainDate、ZonedDateTimeなど一目で分かる
  3. 直感的 → 月が1始まり、メソッド名も分かりやすい
  4. タイムゾーンがファーストクラス → サマータイムも含めて自動処理
  5. 日付計算が正しい → やっと期待通りに動く

次のステップ:

  1. Polyfillで今日から試してみる
  2. 新規プロジェクトにはTemporalを使う
  3. moment.jsからの脱却を計画する
  4. ブラウザの実装状況をウォッチする

Dateとの戦いは、もうすぐ終わります。Temporal時代の幕開けです!🚀

javascripttemporal-apidate-timeecmascriptweb-developmenttypescriptmigration

関連ツールを見る

Pockitの無料開発者ツールを試してみましょう