date-fns v4 日時処理ガイド
date-fns v4 で日時を扱う実装ノート。immutable な関数 API の扱い方(parseISO / format / addDays / differenceInDays)、ロケール / i18n まわりの実装(日本語ロケール + タイムゾーン考慮)までを 1 ページに統合。
date-fns v4 でイミュータブルに日付を扱う
date-fns v4 系の関数型 API(format / parseISO / addDays / differenceInDays / formatDistanceToNow / locale)を最小コードと触れる demo で確認する実装メモ。
検証日: 2026-05-09
使用バージョン:
date-fns@4.1.0対象: ブラウザ / Node で日付を扱う TypeScript プロジェクト、Day.js / Moment からの移行検討中の人
date-fns v4 の イミュータブル関数型 API(format / parseISO / addDays / differenceInDays / formatDistanceToNow)を最小コードで使う記事。動く demo で各関数の挙動が即時確認できます。
触って試す
- B − A(日数差)
- 0 日
- A + 7 日
- 2026-05-26 (火曜日)
- A の今日比
- 約9時間前
- A は週末?
- no
なぜ date-fns(v4)を選ぶか
- イミュータブル:すべての関数が新しい
Dateを返す。Moment.js のような chain mutation バグが起きない - Tree-shake 可能:必要な関数だけ import すれば bundle が小さい
- 関数型 API:
format(date, fmt)のスタイルで、Dateはそのまま使える(独自オブジェクトで包まない) - i18n はサブモジュール:
date-fns/locale/jaをインポートして渡す(多言語 / 営業日 / 曜日依存ロジックは /articles/date-fns-v4-locale-i18n/ で深掘り)
Moment.js の moment(d).add(7, 'days') は内部で d を変える ので、共有された moment オブジェクトを後から format するとずれる事故が起きた。date-fns は新しい Date を返すだけなので、そういう副作用バグが起きない。
代替候補と比較:
| 選択肢 | 特徴 |
|---|---|
| date-fns | 関数型、tree-shake、Date ベース(現代的標準) |
| Day.js | API は moment 風、軽量。chain 派 |
| Luxon | DateTime クラスとタイムゾーン強い |
標準 Intl.DateTimeFormat | format だけなら追加 dep 不要 |
Temporal(stage 3) | 標準化進行中、polyfill 必要 |
Temporal が完全 stable になるまでは date-fns が現実的選択。
主な関数
実務で使う関数の組合せ例:parseISO で文字列→Date、format で表示用文字列、addDays / differenceInDays で計算、formatDistanceToNow で相対表現:
import { format, parseISO, addDays, differenceInDays, formatDistanceToNow, isWeekend } from "date-fns";
import { ja } from "date-fns/locale";
const d = parseISO("2026-05-09"); // ISO 文字列 → Date
format(d, "yyyy/MM/dd (EEEE)", { locale: ja }); // "2026/05/09 (土曜日)"
addDays(d, 7); // 1 週間後の Date
differenceInDays(d, parseISO("2026-01-01")); // 128
formatDistanceToNow(d, { addSuffix: true, locale: ja }); // "1 ヶ月後"
isWeekend(d); // true(土日判定)
i18n(locale)
date-fns/locale から対象言語の locale を import して、format(date, token, { locale }) の第 3 引数で渡す:
import { format } from "date-fns";
import { ja, enUS } from "date-fns/locale";
format(new Date(), "PPPP", { locale: ja }); // "2026年5月9日土曜日"
format(new Date(), "PPPP", { locale: enUS }); // "Saturday, May 9th, 2026"
PPPP のような長形式は locale token に置き換わる。yyyy-MM-dd のような直接指定はそのまま動く。
つまずいたポイント
formatの token は Moment と微妙に違う(YYYYではなくyyyy、DDではなくdd)。古い記事のコードがそのまま動かないことがある- タイムゾーン未対応:タイムゾーン横断の操作は
date-fns-tzが別途必要 new Date(string)の挙動はブラウザ依存:ISO 文字列はparseISOを使うほうが確実formatの文字列内で literal を入れる時は'(シングルクォート)で囲む —format(d, "yyyy'年'MM'月'dd'日'")のように
向く / 向かないケース
- 向く: 表示時の format / 単純な日付計算 / i18n
- 向かない: タイムゾーン中心の業務(
date-fns-tzか Luxon を検討) - 向かない: 営業日 / 祝日カレンダー(別ライブラリと組み合わせ)
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
JavaScriptによるはじめてのアルゴリズム入門
Python と JavaScriptではじめるデータビジュアライゼーション
React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際
「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…
はじめてのWebデザイン&プログラミング : HTML、CSS、JavaScript、PHPの基本
フロントエンドの知識地図ーー 一冊でHTML/CSS/JavaScriptの開発技術が学べる本
date-fns v4 で多言語と曜日 / 営業日を扱う
date-fns v4 の locale サブモジュールを使って format / formatRelative / startOfWeek の挙動を i18n 切替、addBusinessDays / differenceInBusinessDays の営業日計算、format token の locale 依存を触れる demo で確認する実装メモ。
検証日: 2026-05-10
使用バージョン:
date-fns@4.1.0対象: 入門は通っている、多言語サイト / 営業日カレンダー / 曜日依存ロジックを書きたい人
入門記事(format / parseISO / addDays)は別途。本稿は i18n と曜日 / 営業日 の中級パターン。
触って試す
| token | 説明 | 出力 |
|---|---|---|
| PPPP | 完全長(曜日 + 月日) | 2026年5月10日日曜日 |
| PPP | 中長(月日 + 年) | 2026年5月10日 |
| PP | 短(月日 + 年) | 2026/05/10 |
| P | 最短(数値日付) | 2026/05/10 |
| yyyy-MM-dd EEEE | ISO 風 + 曜日 | 2026-05-10 日曜日 |
| yyyy 年 M 月 d 日 (EEEE) | 和文混じり | 2026 年 5 月 10 日 (日曜日) |
locale を切り替えると、format token の出力が言語ごとに変わるのが直感的に分かる。
1. locale サブモジュールの import
date-fns/locale から個別の locale(ja / enUS 等)を import し、format 関数の第 3 引数 { locale } で渡す:
import { format, formatDistanceToNow, formatRelative } from "date-fns";
import { ja, enUS, zhCN, ko, fr, de } from "date-fns/locale";
format(new Date(), "PPPP", { locale: ja }); // "2026年5月10日日曜日"
format(new Date(), "PPPP", { locale: enUS }); // "Sunday, May 10th, 2026"
format(new Date(), "PPPP", { locale: zhCN }); // "2026年5月10日 星期日"
ポイント:
date-fns/localeから個別 import:import * as locale from "date-fns/locale"は bundle 全体を取り込むので避けるenUS/enGB/enCAを使い分け:アメリカは月日年、イギリスは日月年など{ locale }を毎回渡す手間が嫌なら独自 wrapper:fmt(date, "PPPP")のような薄いラッパを作る
2. localized format token(P / PP / PPP / PPPP)
長さで意味が変わる token:
| token | en-US | ja | zh-CN |
|---|---|---|---|
P | 05/10/2026 | 2026/05/10 | 2026/05/10 |
PP | May 10, 2026 | 2026年05月10日 | 2026年5月10日 |
PPP | May 10th, 2026 | 2026年5月10日 | 2026年5月10日 |
PPPP | Sunday, May 10th, 2026 | 2026年5月10日日曜日 | 2026年5月10日 星期日 |
yyyy-MM-dd のような直接 token は locale 不依存(数字並びがそのまま)。
P / PP / PPP / PPPP は locale 依存で、一覧やカードのコンパクト表示に推奨。
つまり 同じ token でも出力が locale ごとに大きく変わる のは、format 内部が「pattern 引く → localize で翻訳」の 2 段になっているから。
3. formatRelative / formatDistanceToNow
「いま」を起点とした人間に優しい時間表現:
formatDistanceToNow(date, { addSuffix: true, locale: ja });
// "1分前" / "2日後" / "約1ヶ月前"
formatRelative(date, new Date(), { locale: ja });
// "今日 13:45" / "昨日 09:00" / "先週金曜日 18:30" / "2025/12/01"
- formatDistanceToNow = 大まかな相対時間(N 分前 / N 日前 / 約 N ヶ月前)
- formatRelative = 6 日以内は曜日ベース、それ以前は絶対日付
UI のタイムスタンプ表示は formatRelative + tooltip で絶対日時、が王道。
4. 週始まりの locale 依存
startOfWeek / endOfWeek は locale ごとに「週の始まり」が変わる(日本 / 米国 = 日曜、欧州 = 月曜 等):
import { startOfWeek, endOfWeek } from "date-fns";
import { ja, enUS } from "date-fns/locale";
startOfWeek(date, { locale: ja }); // 月曜始まり
startOfWeek(date, { locale: enUS }); // 日曜始まり
startOfWeek(date, { weekStartsOn: 6 }); // 明示的に土曜始まり(Saudi Arabia 等)
ポイント:
- 明示しないとデフォルト = 日曜始まり(米国ベース)
- 業務カレンダーが月曜始まりなら locale: ja か weekStartsOn: 1
- endOfWeek も同じ option を渡す:組で揃える
5. addBusinessDays / differenceInBusinessDays(営業日)
土日を除いた日付計算:
import { addBusinessDays, differenceInBusinessDays } from "date-fns";
addBusinessDays(new Date("2026-05-08"), 5); // 2026-05-15 (金)
// 5/8(金) → 5/9 5/10 を skip → 5/11 5/12 5/13 5/14 5/15
differenceInBusinessDays(end, start); // 営業日換算の日数差
簡易な祝日対応ラッパー例:
import HolidayJp from "@holiday-jp/holiday_jp";
function isBusinessDay(d: Date): boolean {
const day = d.getDay();
if (day === 0 || day === 6) return false;
if (HolidayJp.isHoliday(d)) return false;
return true;
}
function addBusinessDaysJp(date: Date, n: number): Date {
let cur = date;
let added = 0;
while (added < n) {
cur = addDays(cur, 1);
if (isBusinessDay(cur)) added++;
}
return cur;
}
6. タイムゾーン横断は date-fns-tz
date-fns 本体は タイムゾーン非対応(マシンの local TZ で扱う)。タイムゾーン横断ロジックは date-fns-tz を別途追加:
import { format, toZonedTime, fromZonedTime } from "date-fns-tz";
const utc = new Date("2026-05-10T15:30:00Z");
const tokyo = toZonedTime(utc, "Asia/Tokyo");
format(tokyo, "yyyy-MM-dd HH:mm zzz", { timeZone: "Asia/Tokyo" });
// "2026-05-11 00:30 GMT+9"
UTC で送って 表示時にローカル TZ へ変換 するのが鉄則。new Date(string) の暗黙ローカル化は再現性が壊れやすい。
つまずいたポイント
- format token の大文字小文字:
YYYYではなくyyyy、DDではなくdd(Moment との違い) - literal を含めたい時は
'(シングルクォート)で囲む:format(d, "yyyy'年'MM'月'dd'日'") - formatRelative の境界:6 日を超えると曜日表示から絶対日付に切り替わる(locale ごとに微妙に違う)
{ locale: ja }を渡し忘れる:英語表示になっても無音で fallback。表示確認で気付く- bundle サイズ:全 locale を取り込むと数百 KB。個別 import + tree-shake が必須
- タイムゾーン:date-fns 本体は local TZ 固定。混在を避けるなら ISO 8601 (UTC) で persist
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
JavaScriptによるはじめてのアルゴリズム入門
Python と JavaScriptではじめるデータビジュアライゼーション
React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際
「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…