JavaScript Temporal API完全入門【Date廃止後の新標準2026年版】


はじめに

JavaScriptの Date オブジェクトは長年にわたって開発者を悩ませてきた。

タイムゾーン処理の複雑さ、ミュータブルな設計、直感的でないAPIなど、多くの問題がある。これを解決するために TC39 で策定が進んでいるのが Temporal API だ。

2026年現在、Temporal APIは Stage 3(候補段階)にあり、主要ブラウザでフラグ付きで利用可能になっている。


なぜ Date は問題だったのか

問題1: ミュータブル設計

// Dateオブジェクトはミュータブル(変更可能)
const date = new Date('2026-03-10');
const nextWeek = date;

// 破壊的変更!dateもnextWeekも同じオブジェクトを参照
nextWeek.setDate(date.getDate() + 7);
console.log(date.toDateString()); // 意図せず変わっている!

問題2: タイムゾーンの扱いが難しい

// Dateはタイムゾーンを直接サポートしない
const date = new Date('2026-03-10T00:00:00');
// これはローカルタイム?UTC?JST?コンテキストで変わる

問題3: 月が0始まりという罠

// 月は0始まり(0 = 1月、11 = 12月)
const date = new Date(2026, 2, 10); // 実は2026年3月10日
// 直感的でない!バグの温床

問題4: 日時計算が面倒

// 「30日後」を計算するのにこんなに書く必要がある
const date = new Date();
const futureDate = new Date(date.getTime() + 30 * 24 * 60 * 60 * 1000);

Temporal API の基本型

Temporal APIには複数の型があり、それぞれ異なる用途に使う。

説明
Temporal.PlainDateタイムゾーンなしの日付2026-03-10
Temporal.PlainTimeタイムゾーンなしの時刻14:30:00
Temporal.PlainDateTimeタイムゾーンなしの日時2026-03-10T14:30:00
Temporal.ZonedDateTimeタイムゾーンありの日時2026-03-10T14:30:00+09:00[Asia/Tokyo]
Temporal.InstantUTC基準のタイムスタンプ1741600200000000000(ナノ秒)
Temporal.Duration時間の長さP30DT4H(30日4時間)
Temporal.PlainMonthDay月と日(年なし)—03-10(3月10日)
Temporal.PlainYearMonth年と月2026-03

PlainDate: タイムゾーンなしの日付処理

日付のみ(時刻・タイムゾーン不要)のケースで使う。

// 日付の作成
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // "2026-03-10"

const specificDate = Temporal.PlainDate.from('2026-12-25');
const fromParts = new Temporal.PlainDate(2026, 3, 10); // 月が1始まり!

// イミュータブル: 元のオブジェクトは変わらない
const tomorrow = today.add({ days: 1 });
console.log(today.toString());    // "2026-03-10" (変わらない)
console.log(tomorrow.toString()); // "2026-03-11"

// 日付計算
const birthday = Temporal.PlainDate.from('1990-05-15');
const now = Temporal.Now.plainDateISO();
const age = now.since(birthday);
console.log(age.years); // 35 (2026年時点)

// 日付比較
const date1 = Temporal.PlainDate.from('2026-03-10');
const date2 = Temporal.PlainDate.from('2026-04-01');
console.log(Temporal.PlainDate.compare(date1, date2)); // -1 (date1 < date2)
console.log(date1.equals(date2)); // false

// 月末を取得
const march2026 = Temporal.PlainDate.from('2026-03-01');
const lastDay = march2026.with({ day: march2026.daysInMonth });
console.log(lastDay.toString()); // "2026-03-31"

ZonedDateTime: タイムゾーンを考慮した日時処理

タイムゾーンを扱う必要がある場合は ZonedDateTime を使う。

// 現在の日時をタイムゾーン付きで取得
const tokyoNow = Temporal.Now.zonedDateTimeISO('Asia/Tokyo');
console.log(tokyoNow.toString());
// "2026-03-10T14:30:00+09:00[Asia/Tokyo]"

// タイムゾーン変換
const nycTime = tokyoNow.withTimeZone('America/New_York');
console.log(nycTime.toString());
// "2026-03-10T01:30:00-04:00[America/New_York]" (同じ瞬間)

// 特定の日時をタイムゾーン付きで作成
const meeting = Temporal.ZonedDateTime.from({
  year: 2026,
  month: 3,
  day: 15,
  hour: 10,
  minute: 0,
  timeZone: 'Asia/Tokyo',
});

// サマータイムを自動考慮
const losAngeles = Temporal.ZonedDateTime.from('2026-03-08T10:00:00-08:00[America/Los_Angeles]');
const dayLater = losAngeles.add({ days: 1 });
// アメリカのDST切り替え日でも正確に計算される

Duration: 期間の計算

// Durationの作成
const twoWeeks = Temporal.Duration.from({ weeks: 2 });
const threeMonths = new Temporal.Duration(0, 3); // (years, months)
const complex = Temporal.Duration.from('P1Y2M3DT4H5M6S'); // ISO 8601形式

// 期間の加減算
const start = Temporal.PlainDate.from('2026-01-01');
const afterThreeMonths = start.add(threeMonths);
console.log(afterThreeMonths.toString()); // "2026-04-01"

// 2つの日付の差
const date1 = Temporal.PlainDate.from('2026-01-01');
const date2 = Temporal.PlainDate.from('2026-12-31');
const diff = date1.until(date2, { largestUnit: 'month' });
console.log(diff.months); // 11
console.log(diff.days);   // 30

// 期間の比較
const d1 = Temporal.Duration.from({ days: 30 });
const d2 = Temporal.Duration.from({ months: 1 });
// 注意: 月は固定日数でないため直接比較できない
// 特定の基準日が必要

Instant: 機械的な時刻処理

Instantは「ある瞬間」をUTCのナノ秒精度で表す。

// 現在時刻のInstant
const now = Temporal.Now.instant();
console.log(now.epochMilliseconds); // Dateのgettime()相当
console.log(now.epochNanoseconds);  // ナノ秒精度(BigInt)

// Dateとの相互変換
const oldDate = new Date('2026-03-10T00:00:00Z');
const instant = Temporal.Instant.fromEpochMilliseconds(oldDate.getTime());
console.log(instant.toString()); // "2026-03-10T00:00:00Z"

// Instantからタイムゾーン付きに変換
const tokyo = instant.toZonedDateTimeISO('Asia/Tokyo');
console.log(tokyo.toString());
// "2026-03-10T09:00:00+09:00[Asia/Tokyo]"

// 2つのInstantの差
const start = Temporal.Instant.from('2026-03-10T00:00:00Z');
const end = Temporal.Instant.from('2026-03-10T12:00:00Z');
const diff = end.since(start);
console.log(diff.hours); // 12

実践例: よくある日付処理をTemporalで書く

例1: 日付フォーマット

// Temporal + Intl.DateTimeFormat の組み合わせ
const date = Temporal.PlainDate.from('2026-03-10');

const formatter = new Intl.DateTimeFormat('ja-JP', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  calendar: 'japanese',
});

// PlainDateをDateに変換してから (現在の実装ではこの手順が必要)
const jsDate = new Date(date.toString());
console.log(formatter.format(jsDate));
// "2026年3月10日"

// または単純なフォーマット
console.log(`${date.year}年${date.month}月${date.day}日`);

例2: 業務日の計算(週末スキップ)

function addBusinessDays(date, days) {
  let current = date;
  let added = 0;

  while (added < days) {
    current = current.add({ days: 1 });

    // 土曜(6)・日曜(7)はスキップ
    if (current.dayOfWeek <= 5) {
      added++;
    }
  }

  return current;
}

const today = Temporal.PlainDate.from('2026-03-10'); // 火曜日
const deadline = addBusinessDays(today, 5);
console.log(deadline.toString()); // "2026-03-17" (月曜日)

例3: 年齢計算

function calculateAge(birthday, referenceDate = Temporal.Now.plainDateISO()) {
  const birth = Temporal.PlainDate.from(birthday);

  // 今年の誕生日
  const thisYearBirthday = birth.with({ year: referenceDate.year });

  // 誕生日が過ぎているかチェック
  const hasHadBirthdayThisYear = Temporal.PlainDate.compare(
    referenceDate,
    thisYearBirthday
  ) >= 0;

  const age = referenceDate.year - birth.year - (hasHadBirthdayThisYear ? 0 : 1);
  return age;
}

console.log(calculateAge('1990-05-15'));  // 35 (2026-03-10時点)
console.log(calculateAge('1990-01-01'));  // 36

例4: カウントダウン

function countdown(targetDate) {
  const target = Temporal.PlainDate.from(targetDate);
  const today = Temporal.Now.plainDateISO();

  const diff = today.until(target, {
    largestUnit: 'day',
    smallestUnit: 'day',
  });

  if (diff.days < 0) {
    return `${Math.abs(diff.days)}日経過`;
  } else if (diff.days === 0) {
    return '今日です!';
  } else {
    return `あと${diff.days}日`;
  }
}

console.log(countdown('2026-12-25')); // "あと○○日"

例5: タイムゾーンをまたいだ会議時刻の表示

// 東京時間で14時に予定した会議を、各拠点の時間で表示
function showMeetingTimes(tokyoDateTime) {
  const tokyo = Temporal.ZonedDateTime.from(
    `${tokyoDateTime}[Asia/Tokyo]`
  );

  const timezones = {
    '東京': 'Asia/Tokyo',
    'ニューヨーク': 'America/New_York',
    'ロンドン': 'Europe/London',
    'シドニー': 'Australia/Sydney',
  };

  Object.entries(timezones).forEach(([city, tz]) => {
    const localTime = tokyo.withTimeZone(tz);
    const formattedTime = `${localTime.hour}:${String(localTime.minute).padStart(2, '0')}`;
    console.log(`${city}: ${formattedTime}`);
  });
}

showMeetingTimes('2026-03-10T14:00:00');
// 東京: 14:00
// ニューヨーク: 01:00
// ロンドン: 05:00
// シドニー: 16:00

現時点での使い方(ポリフィル)

Temporal APIはまだネイティブサポートが全てのブラウザで完全ではないため、現時点では @js-temporal/polyfill を使う。

npm install @js-temporal/polyfill
// TypeScriptでの使い方
import { Temporal } from '@js-temporal/polyfill';

const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // "2026-03-10"
// tsconfig.json に "lib": ["ESNext.Temporal"] を追加
{
  "compilerOptions": {
    "lib": ["ES2023", "ESNext.Temporal"]
  }
}

Dateとの比較まとめ

機能DateTemporal
イミュータブル×
タイムゾーンサポート△(手動変換必要)◎(ZonedDateTimeで完全対応)
月が1始まり× (0始まり)◎ (1始まり)
型の明確さ× (全て混在)◎ (用途別に分離)
日付計算の直感性
ナノ秒精度× (ミリ秒まで)
比較メソッド× (手動変換)◎ (compare/equals)

まとめ

Temporal APIは、長年JavaScriptの弱点だった日付・時刻処理を根本から改善する仕様だ。

今すぐ始める方法

  1. @js-temporal/polyfill をインストール
  2. 新規プロジェクトではTemporalを使い始める
  3. 既存の Date コードは段階的に移行

特にタイムゾーンを扱うアプリケーション複雑な日付計算が必要なシステムでは、Temporalに移行することで大幅にコードがシンプルになる。


参考リンク


関連記事