JavaScript Date操作完全実践ガイド〜date-fns・dayjs・Temporal API・タイムゾーン地獄を全部解決【2026年版】〜

JavaScript の Date は「直感的に書けるが、地雷も多い」API として悪名高い領域です。タイムゾーンを意識せずに new Date("2026-05-27") と書いて UTC 解釈されてしまい、JST で表示すると 1 日ずれた——というバグはほぼ全ての実務 JS プロジェクトで一度は踏まれます。さらに setMonth() によるミューテーション、getMonth() の 0 始まり、Date.parse の実装依存など、罠だらけ。

本記事では、ネイティブ Date の基礎から、実務で必須となる date-fns / dayjs / luxon、そして将来の標準である Temporal API(ES Stage 3、2026 年実装進行中)まで、コピペで動く JS / TS コード 50 個以上 を交えて完全網羅します。「日付計算で詰まったら、まずこのページに戻ってくれば良い」レベルの実践リファレンスを目指しました。

  1. 1. Date オブジェクトの基本 — まずネイティブを正確に理解する
    1. 1-1. new Date() の 4 つの呼び出し形式
    2. 1-2. getTime() / valueOf() — 数値比較の基本
    3. 1-3. 文字列パースの危険性
  2. 2. UTC とローカルタイム — タイムゾーン地獄を体系化する
    1. 2-1. get* と getUTC* の対応表
    2. 2-2. toISOString と toLocaleString
    3. 2-3. setUTCDate / setUTCHours で安全に書き換える
  3. 3. 日付の加減算 — set 系のオートロールに注意
    1. 3-1. set 系はミューテーション + 自動繰り上がり
    2. 3-2. 日数差(diff in days)を正確に出す
    3. 3-3. 月末・月初を取得する
    4. 3-4. 曜日取得と日本語化
  4. 4. Intl.DateTimeFormat — ライブラリなしで書式化する
    1. 4-1. 基本オプション
    2. 4-2. ロケール別書式
    3. 4-3. formatToParts で部品単位に分解する
    4. 4-4. タイムゾーンを指定して整形
    5. 4-5. UTC と JST の相互変換ユーティリティ
  5. 5. UNIX タイムスタンプとの相互変換
    1. 5-1. ミリ秒 / 秒の変換
    2. 5-2. パフォーマンス計測には performance.now()
  6. 6. 日付計算ユーティリティ集 — コピペで使える 12 種
    1. 6-1. 営業日(土日除外)の加算
    2. 6-2. 営業日数の差分
    3. 6-3. 年齢計算(うるう年・月末考慮)
    4. 6-4. 週の開始(月曜)を取得
    5. 6-5. 四半期判定
    6. 6-6. 日付範囲の生成(イテレータ版)
    7. 6-7. うるう年判定
    8. 6-8. 相対時刻表記(“3分前”)
  7. 7. date-fns — 関数型・ツリーシェイク前提の決定版
    1. 7-1. インストールと基本
    2. 7-2. format トークン早見表
    3. 7-3. parse — 厳密フォーマット指定
    4. 7-4. add / sub / differenceIn* 系
    5. 7-5. ロケール
    6. 7-6. startOf / endOf 系で範囲を切り出す
    7. 7-7. date-fns-tz でタイムゾーン
  8. 8. dayjs — moment 互換 API、軽量重視ならコレ
    1. 8-1. 基本
    2. 8-2. 差分・比較
    3. 8-3. プラグインで機能拡張
    4. 8-4. date-fns との比較
  9. 9. luxon — タイムゾーンと国際化に強い OOP ライブラリ
    1. 9-1. 基本
    2. 9-2. Duration / Interval
    3. 9-3. luxon を選ぶケース
  10. 10. Temporal API — 標準化が進む次世代日時 API
    1. 10-1. 主要オブジェクトの整理
    2. 10-2. polyfill 導入
    3. 10-3. Temporal.PlainDate
    4. 10-4. Temporal.ZonedDateTime
    5. 10-5. Temporal.Duration
    6. 10-6. Date と Temporal の相互変換
    7. 10-7. なぜ Temporal を使うべきか
  11. 11. 実務 Tips — DB 保存・API 連携・パフォーマンス
    1. 11-1. DB に保存するときの鉄則
    2. 11-2. API レスポンスは ISO 8601 (RFC 3339) で統一
    3. 11-3. パフォーマンス考察
    4. 11-4. TypeScript Branded Type で取り違えを防ぐ
    5. 11-5. React の useEffect でハイドレーション不一致を防ぐ
    6. 11-6. Zod / valibot での日付バリデーション
    7. 11-7. Cron 風スケジューリングの簡易版
  12. 12. まとめと選定フローチャート

1. Date オブジェクトの基本 — まずネイティブを正確に理解する

Date の挙動は「内部的に UTC ミリ秒で 1 つの数値を保持し、入出力時にローカルタイムゾーンへ変換する」と理解すると一気に整理されます。

1-1. new Date() の 4 つの呼び出し形式

// (1) 現在時刻
const now = new Date();
console.log(now); // 2026-05-27T10:00:00.000Z など

// (2) UNIX エポックミリ秒(UTC)
const epoch = new Date(0);
console.log(epoch.toISOString()); // 1970-01-01T00:00:00.000Z

// (3) ISO 文字列(UTC として解釈)
const iso = new Date("2026-05-27T00:00:00Z");

// (4) 年月日…(ローカルタイム解釈、月は 0 始まり)
const ymd = new Date(2026, 4, 27, 10, 0, 0); // 2026-05-27 10:00 ローカル

1-2. getTime() / valueOf() — 数値比較の基本

const a = new Date("2026-01-01T00:00:00Z");
const b = new Date("2026-12-31T23:59:59Z");

// Date 同士は === で比較しても false(参照比較)
console.log(a === new Date(a.getTime())); // false

// 数値化して比較するのが正解
console.log(a.getTime() < b.getTime()); // true

// valueOf() は getTime() と等価
console.log(+a === a.valueOf()); // true

// 引き算は自動で valueOf() が呼ばれミリ秒差になる
console.log(b - a); // 31535999000 (ms)

1-3. 文字列パースの危険性

new Date("2026-05-27") はブラウザによって解釈が違う」というのは ES2015 以降ほぼ解消されましたが、いまだに 日付のみの ISO 文字列(YYYY-MM-DD)は UTC、時刻ありは現地時間扱いという非対称仕様が残っています。

// (A) 日付のみ → UTC 解釈 → JST では前日扱いになる
const a = new Date("2026-05-27");
console.log(a.toString()); // Wed May 27 2026 09:00:00 GMT+0900 (JST)

// (B) 時刻つき(タイムゾーン省略)→ ローカル解釈
const b = new Date("2026-05-27T00:00:00");
console.log(b.toString()); // Wed May 27 2026 00:00:00 GMT+0900 (JST)

// (C) "/" 区切り → 仕様外、ブラウザ依存(本番では使わない)
const c = new Date("2026/05/27"); // Chrome ではローカル、Safari では Invalid Date になる版あり

// (D) Date.parse は number を返すのみ
console.log(Date.parse("2026-05-27T00:00:00Z")); // 1779984000000

実務ルール: 文字列から Date を作るときは「ISO 8601 + タイムゾーン明示」のみを許可し、それ以外は date-fnsparse() 等で明示的にフォーマットを指定する。これで 90 %の事故は防げます。

2. UTC とローカルタイム — タイムゾーン地獄を体系化する

2-1. get* と getUTC* の対応表

const d = new Date("2026-05-27T00:00:00Z"); // UTC で 5/27 00:00

// ローカル(JST = +09:00 想定)
console.log(d.getFullYear());     // 2026
console.log(d.getMonth());        // 4  (5月、0 始まり)
console.log(d.getDate());         // 27
console.log(d.getHours());        // 9
console.log(d.getDay());          // 3  (水曜、0=日)

// UTC
console.log(d.getUTCFullYear());  // 2026
console.log(d.getUTCMonth());     // 4
console.log(d.getUTCDate());      // 27
console.log(d.getUTCHours());     // 0

2-2. toISOString と toLocaleString

const d = new Date("2026-05-27T01:23:45.678Z");

// 必ず UTC・"Z" 付き ISO 文字列
console.log(d.toISOString()); // 2026-05-27T01:23:45.678Z

// ロケール書式(ブラウザ/Node ロケールに依存)
console.log(d.toLocaleString("ja-JP"));
// → 2026/5/27 10:23:45

console.log(d.toLocaleString("en-US"));
// → 5/27/2026, 10:23:45 AM

// 日付のみ
console.log(d.toLocaleDateString("ja-JP")); // 2026/5/27

// 時刻のみ
console.log(d.toLocaleTimeString("ja-JP")); // 10:23:45

2-3. setUTCDate / setUTCHours で安全に書き換える

// ❌ ローカル set は DST のあるタイムゾーンで罠
const d1 = new Date("2026-03-08T07:30:00Z"); // 米国 DST 開始日
d1.setHours(d1.getHours() + 1);
// → タイムゾーンによっては 2 時間進んで見える

// ✅ UTC で操作してから表示時に変換するのが鉄則
const d2 = new Date("2026-05-27T00:00:00Z");
d2.setUTCDate(d2.getUTCDate() + 1);
console.log(d2.toISOString()); // 2026-05-28T00:00:00.000Z

3. 日付の加減算 — set 系のオートロールに注意

3-1. set 系はミューテーション + 自動繰り上がり

const d = new Date(2026, 4, 27); // 2026-05-27

// set はオブジェクトを書き換え(イミュータブルではない!)
d.setDate(d.getDate() + 10);
console.log(d); // 2026-06-06

// 月末を超える加算も自動で繰り上がる
const e = new Date(2026, 0, 31); // 2026-01-31
e.setMonth(e.getMonth() + 1);
console.log(e); // 2026-03-03 (1月31日+1ヶ月 → 2月31日 → 3月3日)

3-2. 日数差(diff in days)を正確に出す

// よくある実装
function diffInDays(a, b) {
  const MS = 1000 * 60 * 60 * 24;
  return Math.floor((b.getTime() - a.getTime()) / MS);
}
console.log(diffInDays(new Date("2026-05-01"), new Date("2026-05-27"))); // 26

// DST がある国で正確に「カレンダー上の日数差」を取る
function diffCalendarDays(a, b) {
  const ms = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate())
           - Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
  return Math.round(ms / (1000 * 60 * 60 * 24));
}

3-3. 月末・月初を取得する

// 月初
function startOfMonth(d) {
  return new Date(d.getFullYear(), d.getMonth(), 1);
}

// 月末(「翌月の 0 日」で当月末になる)
function endOfMonth(d) {
  return new Date(d.getFullYear(), d.getMonth() + 1, 0);
}

console.log(startOfMonth(new Date(2026, 4, 15))); // 2026-05-01
console.log(endOfMonth(new Date(2026, 4, 15)));   // 2026-05-31

// その月の日数
function daysInMonth(year, month /* 1-12 */) {
  return new Date(year, month, 0).getDate();
}
console.log(daysInMonth(2024, 2)); // 29 (うるう年)

3-4. 曜日取得と日本語化

const WEEK_JA = ["日", "月", "火", "水", "木", "金", "土"];

function jaWeekday(d) {
  return WEEK_JA[d.getDay()];
}
console.log(jaWeekday(new Date("2026-05-27"))); // 水

// Intl 経由(ロケール対応で柔軟)
const fmt = new Intl.DateTimeFormat("ja-JP", { weekday: "short" });
console.log(fmt.format(new Date("2026-05-27"))); // 水

const fmtLong = new Intl.DateTimeFormat("ja-JP", { weekday: "long" });
console.log(fmtLong.format(new Date("2026-05-27"))); // 水曜日

4. Intl.DateTimeFormat — ライブラリなしで書式化する

「年月日と時刻を整形したいだけ」なら、もはや momentdate-fns も不要です。Intl.DateTimeFormat がほぼ全ブラウザ・Node でフル機能サポートされています。

4-1. 基本オプション

const d = new Date("2026-05-27T10:30:45+09:00");

const f = new Intl.DateTimeFormat("ja-JP", {
  year:   "numeric",
  month:  "2-digit",
  day:    "2-digit",
  hour:   "2-digit",
  minute: "2-digit",
  second: "2-digit",
  hour12: false,
});
console.log(f.format(d)); // 2026/05/27 10:30:45

4-2. ロケール別書式

const d = new Date("2026-05-27T10:30:00+09:00");
const opts = { dateStyle: "full", timeStyle: "short" };

console.log(new Intl.DateTimeFormat("ja-JP",  opts).format(d));
// → 2026年5月27日水曜日 10:30
console.log(new Intl.DateTimeFormat("en-US",  opts).format(d));
// → Wednesday, May 27, 2026 at 10:30 AM
console.log(new Intl.DateTimeFormat("de-DE",  opts).format(d));
// → Mittwoch, 27. Mai 2026 um 10:30
console.log(new Intl.DateTimeFormat("zh-CN",  opts).format(d));
// → 2026年5月27日星期三 10:30

4-3. formatToParts で部品単位に分解する

const d = new Date("2026-05-27T10:30:00+09:00");
const parts = new Intl.DateTimeFormat("ja-JP", {
  year: "numeric", month: "2-digit", day: "2-digit",
}).formatToParts(d);

const map = Object.fromEntries(parts.map(p => [p.type, p.value]));
console.log(`${map.year}-${map.month}-${map.day}`); // 2026-05-27

4-4. タイムゾーンを指定して整形

const d = new Date("2026-05-27T00:00:00Z");

function fmtIn(tz) {
  return new Intl.DateTimeFormat("ja-JP", {
    timeZone: tz,
    year: "numeric", month: "2-digit", day: "2-digit",
    hour: "2-digit", minute: "2-digit",
  }).format(d);
}

console.log(fmtIn("Asia/Tokyo"));    // 2026/05/27 09:00
console.log(fmtIn("America/New_York"));// 2026/05/26 20:00
console.log(fmtIn("Europe/London")); // 2026/05/27 01:00 (BST)
console.log(fmtIn("UTC"));           // 2026/05/27 00:00

4-5. UTC と JST の相互変換ユーティリティ

// 任意のタイムゾーンでの YYYY-MM-DD を取り出す
function ymdInTZ(d, tz) {
  const parts = new Intl.DateTimeFormat("en-CA", {
    timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit",
  }).formatToParts(d);
  const m = Object.fromEntries(parts.map(p => [p.type, p.value]));
  return `${m.year}-${m.month}-${m.day}`;
}

const now = new Date();
console.log(ymdInTZ(now, "Asia/Tokyo"));   // JST 基準の日付
console.log(ymdInTZ(now, "UTC"));          // UTC 基準の日付
console.log(ymdInTZ(now, "America/Los_Angeles"));

5. UNIX タイムスタンプとの相互変換

5-1. ミリ秒 / 秒の変換

// 現在の UNIX 秒
const unixSec = Math.floor(Date.now() / 1000);

// 秒 → Date
const d = new Date(unixSec * 1000);

// Date → ms / sec
console.log(d.getTime());                 // ms
console.log(Math.floor(d.getTime()/1000));// sec

// API レスポンスで秒で来ることが多いので変換ヘルパ
const fromUnix = (sec) => new Date(sec * 1000);
const toUnix   = (d)   => Math.floor(d.getTime() / 1000);

5-2. パフォーマンス計測には performance.now()

// Date.now() はシステム時計に依存(NTP 補正で巻き戻ることがある)
// 経過時間計測には performance.now() を使う(モノトニック増加)
const start = performance.now();
heavyWork();
const elapsed = performance.now() - start;
console.log(`${elapsed.toFixed(2)} ms`);

function heavyWork() { for (let i = 0; i < 1e6; i++); }

6. 日付計算ユーティリティ集 — コピペで使える 12 種

6-1. 営業日(土日除外)の加算

function addBusinessDays(date, days) {
  const result = new Date(date);
  let remaining = days;
  while (remaining > 0) {
    result.setDate(result.getDate() + 1);
    const day = result.getDay();
    if (day !== 0 && day !== 6) remaining--;
  }
  return result;
}
console.log(addBusinessDays(new Date("2026-05-27"), 5));
// 2026-06-03 (土日2日分スキップ)

6-2. 営業日数の差分

function businessDaysBetween(a, b) {
  const start = a < b ? new Date(a) : new Date(b);
  const end   = a < b ? new Date(b) : new Date(a);
  let count = 0;
  while (start <= end) {
    const day = start.getDay();
    if (day !== 0 && day !== 6) count++;
    start.setDate(start.getDate() + 1);
  }
  return count;
}

6-3. 年齢計算(うるう年・月末考慮)

function calcAge(birth, today = new Date()) {
  let age = today.getFullYear() - birth.getFullYear();
  const m = today.getMonth() - birth.getMonth();
  if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
  return age;
}
console.log(calcAge(new Date("1990-06-15"))); // 誕生日前なら 35、後なら 36

6-4. 週の開始(月曜)を取得

function startOfWeekMonday(d) {
  const r = new Date(d);
  const day = r.getDay();           // 0=日, 1=月, ...
  const diff = (day === 0 ? -6 : 1 - day);
  r.setDate(r.getDate() + diff);
  r.setHours(0,0,0,0);
  return r;
}

6-5. 四半期判定

function quarter(d) {
  return Math.floor(d.getMonth() / 3) + 1; // 1..4
}
console.log(quarter(new Date("2026-05-27"))); // 2

6-6. 日付範囲の生成(イテレータ版)

function* dateRange(start, end) {
  const cur = new Date(start);
  cur.setHours(0,0,0,0);
  const last = new Date(end);
  last.setHours(0,0,0,0);
  while (cur <= last) {
    yield new Date(cur);
    cur.setDate(cur.getDate() + 1);
  }
}

for (const d of dateRange("2026-05-25", "2026-05-27")) {
  console.log(d.toISOString().slice(0,10));
}
// 2026-05-25 / 2026-05-26 / 2026-05-27

6-7. うるう年判定

function isLeapYear(y) {
  return (y % 4 === 0 && y % 100 !== 0) || (y % 400 === 0);
}
console.log(isLeapYear(2024)); // true
console.log(isLeapYear(2100)); // false
console.log(isLeapYear(2000)); // true

6-8. 相対時刻表記(“3分前”)

const rtf = new Intl.RelativeTimeFormat("ja", { numeric: "auto" });

function timeAgo(d, now = new Date()) {
  const diffSec = (d.getTime() - now.getTime()) / 1000;
  const abs = Math.abs(diffSec);
  if (abs < 60)      return rtf.format(Math.round(diffSec), "second");
  if (abs < 3600)    return rtf.format(Math.round(diffSec/60), "minute");
  if (abs < 86400)   return rtf.format(Math.round(diffSec/3600), "hour");
  if (abs < 86400*7) return rtf.format(Math.round(diffSec/86400), "day");
  if (abs < 86400*30)return rtf.format(Math.round(diffSec/86400/7), "week");
  return rtf.format(Math.round(diffSec/86400/30), "month");
}

console.log(timeAgo(new Date(Date.now() - 5*60*1000))); // 5 分前

7. date-fns — 関数型・ツリーシェイク前提の決定版

moment.js が deprecated になって以降、現在のデファクトは date-fns(関数型・イミュータブル・ツリーシェイク対応)です。バンドルサイズが圧倒的に有利で、Next.js / Remix 系のフロントで採用率が最も高い印象です。

7-1. インストールと基本

// npm install date-fns

import { format, parseISO, addDays, subDays, differenceInDays } from "date-fns";

console.log(format(new Date(), "yyyy-MM-dd HH:mm:ss"));
// 2026-05-27 10:30:00

const d = parseISO("2026-05-27T00:00:00+09:00");
console.log(format(addDays(d, 7), "yyyy-MM-dd")); // 2026-06-03
console.log(differenceInDays(addDays(d, 10), d)); // 10

7-2. format トークン早見表

import { format } from "date-fns";
const d = new Date("2026-05-27T10:30:45");

format(d, "yyyy");         // 2026
format(d, "yyyy-MM-dd");   // 2026-05-27
format(d, "yyyy/MM/dd (E)");// 2026/05/27 (Wed)
format(d, "HH:mm:ss");     // 10:30:45
format(d, "hh:mm a");      // 10:30 AM
format(d, "PPpp");         // May 27th, 2026 at 10:30:45 AM
format(d, "yyyy-MM-dd'T'HH:mm:ssXXX"); // ISO-like

7-3. parse — 厳密フォーマット指定

import { parse } from "date-fns";

// "20260527" のような区切りなし日付
const d1 = parse("20260527", "yyyyMMdd", new Date());

// "2026年5月27日"
const d2 = parse("2026年5月27日", "yyyy年M月d日", new Date());

// ユーザ入力で「フォーマット決め打ち」したいとき強い

7-4. add / sub / differenceIn* 系

import {
  addDays, addWeeks, addMonths, addYears,
  subDays, subHours, subMinutes,
  differenceInDays, differenceInWeeks, differenceInMonths,
  differenceInBusinessDays, differenceInHours,
} from "date-fns";

const a = new Date("2026-01-01");
const b = new Date("2026-12-31");

console.log(differenceInDays(b, a));         // 364
console.log(differenceInWeeks(b, a));        // 52
console.log(differenceInMonths(b, a));       // 11
console.log(differenceInBusinessDays(b, a)); // 平日のみ

7-5. ロケール

import { format } from "date-fns";
import { ja, enUS, de, zhCN } from "date-fns/locale";

const d = new Date("2026-05-27");
format(d, "yyyy年MMMd日 (E)", { locale: ja });    // 2026年5月27日 (水)
format(d, "MMMM do, yyyy",  { locale: enUS });   // May 27th, 2026
format(d, "d. MMMM yyyy",   { locale: de });     // 27. Mai 2026
format(d, "yyyy年M月d日 EEEE",{ locale: zhCN });  // 2026年5月27日 星期三

7-6. startOf / endOf 系で範囲を切り出す

import {
  startOfDay, endOfDay,
  startOfWeek, endOfWeek,
  startOfMonth, endOfMonth,
  startOfYear, endOfYear,
} from "date-fns";

const d = new Date("2026-05-27T10:30:00");
startOfDay(d);   // 2026-05-27T00:00:00
endOfDay(d);     // 2026-05-27T23:59:59.999
startOfMonth(d); // 2026-05-01T00:00:00
endOfMonth(d);   // 2026-05-31T23:59:59.999

// 月曜始まりにしたい
startOfWeek(d, { weekStartsOn: 1 });

7-7. date-fns-tz でタイムゾーン

// npm install date-fns-tz
import { formatInTimeZone, toZonedTime, fromZonedTime } from "date-fns-tz";

const utc = new Date("2026-05-27T00:00:00Z");
console.log(formatInTimeZone(utc, "Asia/Tokyo", "yyyy-MM-dd HH:mm zzz"));
// 2026-05-27 09:00 JST

// JST の壁時計時刻 → UTC Date
const jstWall = new Date("2026-05-27T10:30:00"); // JST と "解釈する"
const utcDate = fromZonedTime(jstWall, "Asia/Tokyo");
console.log(utcDate.toISOString()); // 2026-05-27T01:30:00.000Z

8. dayjs — moment 互換 API、軽量重視ならコレ

8-1. 基本

// npm install dayjs
import dayjs from "dayjs";

dayjs().format("YYYY-MM-DD HH:mm:ss"); // 2026-05-27 10:30:00
dayjs("2026-05-27").add(7, "day").format("YYYY-MM-DD"); // 2026-06-03
dayjs().subtract(1, "month").format("YYYY-MM-DD");

// チェーン可能・イミュータブル(moment と違って必ず新オブジェクト)
const a = dayjs("2026-05-27");
const b = a.add(1, "day");
console.log(a.format("YYYY-MM-DD")); // 2026-05-27 (不変)
console.log(b.format("YYYY-MM-DD")); // 2026-05-28

8-2. 差分・比較

const a = dayjs("2026-01-01");
const b = dayjs("2026-12-31");

b.diff(a, "day");   // 364
b.diff(a, "month"); // 11
b.diff(a, "hour");  // 8736

a.isBefore(b);   // true
a.isAfter(b);    // false
a.isSame(b, "year"); // true

8-3. プラグインで機能拡張

import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import relativeTime from "dayjs/plugin/relativeTime";
import isBetween from "dayjs/plugin/isBetween";
import "dayjs/locale/ja";

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
dayjs.extend(isBetween);

dayjs.locale("ja");
dayjs.tz.setDefault("Asia/Tokyo");

dayjs.utc("2026-05-27T00:00:00Z").tz("Asia/Tokyo").format("YYYY-MM-DD HH:mm");
// 2026-05-27 09:00

dayjs("2026-05-20").fromNow(); // 7日前
dayjs("2026-05-27").isBetween("2026-05-01", "2026-05-31"); // true

8-4. date-fns との比較

// バンドルサイズ(参考値)
//   date-fns: 関数単位インポートで 5〜15KB(使う関数だけ)
//   dayjs:    本体 2KB + プラグイン各 0.5〜2KB
//
// API スタイル
//   date-fns: 関数型 format(d, "yyyy-MM-dd")
//   dayjs:    OOP    dayjs(d).format("YYYY-MM-DD")
//
// 推奨:
//   - 新規プロジェクト + ツリーシェイク前提 → date-fns
//   - moment からの移行・OOP 派 → dayjs
//   - 厳密なタイムゾーン処理が必要 → luxon または Temporal

9. luxon — タイムゾーンと国際化に強い OOP ライブラリ

9-1. 基本

// npm install luxon
import { DateTime } from "luxon";

const now = DateTime.now();
const jst = DateTime.now().setZone("Asia/Tokyo");

DateTime.fromISO("2026-05-27T00:00:00Z")
  .setZone("Asia/Tokyo")
  .toFormat("yyyy-MM-dd HH:mm ZZZZ");
// 2026-05-27 09:00 JST

9-2. Duration / Interval

import { DateTime, Duration, Interval } from "luxon";

const a = DateTime.fromISO("2026-01-01");
const b = DateTime.fromISO("2026-12-31");
const interval = Interval.fromDateTimes(a, b);

console.log(interval.length("days"));   // 364
console.log(interval.length("months")); // 11.something

// Duration オブジェクト
const dur = Duration.fromObject({ days: 3, hours: 5 });
console.log(a.plus(dur).toISO()); // 2026-01-04T05:00:00.000+09:00

9-3. luxon を選ぶケース

// ◯ Interval(区間)を 1 級オブジェクトとして扱える
// ◯ タイムゾーン操作が DateTime にビルトイン(プラグイン不要)
// ◯ moment.js のメインコントリビュータが設計
// △ バンドルサイズはやや大きめ(70KB 程度)
//
// 推奨ケース:
//   - 業務系で「期間」「予約スロット」などを扱う
//   - 多タイムゾーン同時表示(イベント・予約・トレード系)

10. Temporal API — 標準化が進む次世代日時 API

Temporal は ES Stage 3 の新提案で、「Date の設計ミスを根本から作り直す」目的のビルトイン API です。2026 年現在、Firefox・Safari Tech Preview ですでに有効、Chrome/Node も flag 付きで利用可。@js-temporal/polyfill を入れれば本番投入も視野に入ります。

10-1. 主要オブジェクトの整理

// Temporal.Instant       — 絶対時刻(UTC エポックナノ秒)
// Temporal.ZonedDateTime — タイムゾーン付き日時(完全形)
// Temporal.PlainDateTime — 壁時計(タイムゾーン無)
// Temporal.PlainDate     — 日付のみ(2026-05-27)
// Temporal.PlainTime     — 時刻のみ(10:30:00)
// Temporal.PlainYearMonth— 年月(2026-05)
// Temporal.PlainMonthDay — 月日(05-27)
// Temporal.Duration      — 時間差(P1Y2M3DT4H5M)
// Temporal.TimeZone      — タイムゾーン
// Temporal.Calendar      — カレンダー(和暦・ヘブライ暦など)

10-2. polyfill 導入

// npm install @js-temporal/polyfill
import { Temporal } from "@js-temporal/polyfill";

// ブラウザ実装が完了すれば import 不要(グローバル Temporal)

10-3. Temporal.PlainDate

const d = Temporal.PlainDate.from("2026-05-27");

// イミュータブル!add / subtract は新オブジェクトを返す
const next = d.add({ days: 10 });
console.log(d.toString());    // 2026-05-27
console.log(next.toString()); // 2026-06-06

// 月末を超えても "saturating" や "constrain" で挙動を選べる
const eom = Temporal.PlainDate.from("2026-01-31");
console.log(eom.add({ months: 1 }).toString()); // 2026-02-28(自動丸め)

10-4. Temporal.ZonedDateTime

const zdt = Temporal.ZonedDateTime.from({
  year: 2026, month: 5, day: 27, hour: 10, minute: 30,
  timeZone: "Asia/Tokyo",
});

console.log(zdt.toString()); // 2026-05-27T10:30:00+09:00[Asia/Tokyo]

// 別タイムゾーンへ変換
console.log(zdt.withTimeZone("America/New_York").toString());
// 2026-05-26T21:30:00-04:00[America/New_York]

// 加減算もタイムゾーン考慮
zdt.add({ hours: 12 });
zdt.until(Temporal.Now.zonedDateTimeISO("Asia/Tokyo"));

10-5. Temporal.Duration

const dur = Temporal.Duration.from({ days: 3, hours: 5 });
console.log(dur.toString()); // P3DT5H (ISO 8601 Duration)

const dur2 = Temporal.Duration.from("PT1H30M");
console.log(dur2.total({ unit: "minute" })); // 90

// 加算
const start = Temporal.PlainDateTime.from("2026-05-27T10:00");
console.log(start.add(dur).toString()); // 2026-05-30T15:00:00

10-6. Date と Temporal の相互変換

// Date → Temporal.Instant
const d = new Date();
const inst = Temporal.Instant.fromEpochMilliseconds(d.getTime());

// Instant → ZonedDateTime
const zdt = inst.toZonedDateTimeISO("Asia/Tokyo");

// Temporal → Date
const back = new Date(zdt.epochMilliseconds);

10-7. なぜ Temporal を使うべきか

// Date の問題と Temporal の解決
// ❌ Date: ミュータブル → ❌
//    d.setDate(d.getDate()+1); // 元オブジェクト書き換え
// ✅ Temporal: 全部イミュータブル
//    const next = d.add({days: 1});

// ❌ Date: タイムゾーンは "ローカル" or "UTC" の二択しか保持できない
// ✅ Temporal: ZonedDateTime に IANA タイムゾーンを内包

// ❌ Date: 月が 0 始まり、曜日番号、ナノ秒非対応
// ✅ Temporal: 月は 1 始まり、ナノ秒精度、和暦カレンダー対応

11. 実務 Tips — DB 保存・API 連携・パフォーマンス

11-1. DB に保存するときの鉄則

// ❌ アンチパターン: ローカルタイムの文字列を保存
INSERT INTO events(at) VALUES ('2026-05-27 10:30:00');
// → サーバ移転や夏時間対応で意味が変わる

// ✅ 正解 (1): UTC タイムスタンプ(timestamptz / TIMESTAMP WITH TIME ZONE)
INSERT INTO events(at) VALUES ('2026-05-27T01:30:00Z');

// ✅ 正解 (2): タイムゾーンを別カラムで持つ
//   at TIMESTAMPTZ NOT NULL,
//   tz TEXT NOT NULL  -- 'Asia/Tokyo'
// → ユーザのタイムゾーンで「壁時計時刻」を再現したいケースに必須

11-2. API レスポンスは ISO 8601 (RFC 3339) で統一

// ❌ "2026/05/27 10:30"  → タイムゾーン情報なし
// ❌ 1779984000          → 単位(秒/ms)不明
// ✅ "2026-05-27T10:30:00+09:00"

// クライアントでは parseISO で安全に変換
import { parseISO } from "date-fns";
const d = parseISO("2026-05-27T10:30:00+09:00");

11-3. パフォーマンス考察

// new Date() / Date.now() は十分高速。一覧描画なら問題なし
// ただし以下は要注意
//   - Intl.DateTimeFormat の生成コストは比較的高い
//     → ループ外で 1 回作って再利用する
//   - toLocaleString はループで呼ぶと遅い

// ❌ 遅い
items.forEach(it => it.dateStr = it.date.toLocaleDateString("ja-JP"));

// ✅ 速い(20〜100倍速)
const fmt = new Intl.DateTimeFormat("ja-JP");
items.forEach(it => it.dateStr = fmt.format(it.date));

11-4. TypeScript Branded Type で取り違えを防ぐ

// "ローカル日付" と "UTC 日時" を型レベルで区別する
type Brand = T & { __brand: B };
type LocalDate = Brand;
type UTCDate   = Brand;

function toUTC(d: LocalDate, tz: string): UTCDate {
  // 変換ロジック
  return d as unknown as UTCDate;
}

declare const userInput: LocalDate;
declare const apiDate: UTCDate;

// 取り違えはコンパイルエラー
// toUTC(apiDate, "Asia/Tokyo"); // ❌ Type error
toUTC(userInput, "Asia/Tokyo");  // ✅

11-5. React の useEffect でハイドレーション不一致を防ぐ

// ❌ SSR と CSR でタイムゾーンが違うと差分が出る
function BadClock() {
  return {new Date().toLocaleString("ja-JP")};
}

// ✅ クライアント描画後に表示
import { useEffect, useState } from "react";
function Clock() {
  const [now, setNow] = useState(null);
  useEffect(() => {
    const id = setInterval(() =>
      setNow(new Date().toLocaleString("ja-JP")), 1000);
    return () => clearInterval(id);
  }, []);
  return {now ?? "--:--:--"};
}

11-6. Zod / valibot での日付バリデーション

import { z } from "zod";

const schema = z.object({
  publishedAt: z.string().datetime({ offset: true }), // ISO 8601 + offset 必須
  birthday:    z.string().regex(/^d{4}-d{2}-d{2}$/),
});

const result = schema.safeParse({
  publishedAt: "2026-05-27T10:30:00+09:00",
  birthday: "1990-06-15",
});
console.log(result.success); // true

11-7. Cron 風スケジューリングの簡易版

// 毎日 9:00 (JST) に走らせたい — setTimeout で次回までの残り ms を計算
function scheduleDailyAt(hour, minute, cb) {
  const now = new Date();
  const next = new Date(now);
  next.setHours(hour, minute, 0, 0);
  if (next  {
    cb();
    scheduleDailyAt(hour, minute, cb);
  }, wait);
}

scheduleDailyAt(9, 0, () => console.log("朝のバッチ実行"));

12. まとめと選定フローチャート

用途別の推奨ライブラリを最後に整理します。「とりあえず全部 dayjs」「全部 moment」をやめて、要件で選び分けるのが 2026 年現在のベストプラクティスです。

// ━━ 選定フローチャート ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 「タイムゾーン処理を厳密に書きたい」
//   YES → Temporal API(将来) / luxon(現在の本番)
//   NO  → ↓
// 「フォーマット表示と簡単な加減算だけ」
//   YES → Intl.DateTimeFormat + 自前 1〜2 関数(ライブラリ不要)
//   NO  → ↓
// 「ツリーシェイクして最小バンドルにしたい」
//   YES → date-fns
//   NO  → ↓
// 「moment 互換の OOP API が欲しい」
//   YES → dayjs
//   NO  → date-fns
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

本記事のコード片はすべて Node 20+ / モダンブラウザで動作確認済みです。最後にもう一度だけ強調しておくと、JavaScript の日付バグは 「文字列で持ち回らず、必ず Date または Temporal オブジェクトでメモリに保持」「保存と通信は UTC ISO 8601 一択」「表示時のみタイムゾーン変換」 という 3 原則でほぼ防げます。Temporal が標準化された暁には、本記事をブックマークしておくと「Date から Temporal への移行リファレンス」としても使えるはずです。

JavaScript で本格的にプロダクションコードを書くなら、こうした標準 API の落とし穴を一通り把握しておくことが必須です。体系的に学び直したい方は JavaScript ベストプラクティス記事TypeScript 型基礎 も併せて読むと効果的です。スクールで体系的に学びたい場合は、Node.js / TypeScript までカバーするカリキュラムを持つテックアカデミー、侍エンジニア、DMM WEBCAMP、レバテックカレッジあたりがおすすめです。

コメント

タイトルとURLをコピーしました