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. Date オブジェクトの基本 — まずネイティブを正確に理解する
- 2. UTC とローカルタイム — タイムゾーン地獄を体系化する
- 3. 日付の加減算 — set 系のオートロールに注意
- 4. Intl.DateTimeFormat — ライブラリなしで書式化する
- 5. UNIX タイムスタンプとの相互変換
- 6. 日付計算ユーティリティ集 — コピペで使える 12 種
- 7. date-fns — 関数型・ツリーシェイク前提の決定版
- 8. dayjs — moment 互換 API、軽量重視ならコレ
- 9. luxon — タイムゾーンと国際化に強い OOP ライブラリ
- 10. Temporal API — 標準化が進む次世代日時 API
- 11. 実務 Tips — DB 保存・API 連携・パフォーマンス
- 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-fns の parse() 等で明示的にフォーマットを指定する。これで 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 — ライブラリなしで書式化する
「年月日と時刻を整形したいだけ」なら、もはや moment も date-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、レバテックカレッジあたりがおすすめです。

コメント