日付フォーマットをIntl.DateTimeFormatに移行してライブラリの依存を減らす

日付フォーマットといえば、これまで date-fns や dayjs を利用することが多かったと思います。

私も普段は date-fns を利用しており、日付の表示から日付計算まで一通り date-fns に寄せています。

特に困っているわけではなかったのですが、最近仕事で一緒になったメンバーが Intl.DateTimeFormat() を提案してきて初めて知ったので、少し調べてみた。

  • Intl.DateTimeFormat でどこまでできるのか
  • date-fns とどう使い分けられそうか

を整理してみます。

現在の使い方

例えば日付の表示はこんな感じです。

import { format } from "date-fns";

format(date, "yyyy/MM/dd");
format(date, "yyyy/MM/dd HH:mm");
format(date, "MM/dd");

date-fns はフォーマット文字列が分かりやすく、利用している方も多いのではないでしょうか。
ただ、よく考えてみるとフォーマットを調整して表示しているだけのケースも少なくありません。

Intl.DateTimeFormat とは

Intl.DateTimeFormat は JavaScript の標準 API なので、追加のライブラリを導入しなくても利用できます。

例えば日付表示であれば、

const formatter = new Intl.DateTimeFormat("ja-JP", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
});

formatter.format(new Date());

のように利用できます。

良いなと思ったところ

依存ライブラリを増やさなくて良い

最大のメリットはこれ。

例えば、

format(date, "yyyy/MM/dd");

のためだけにライブラリを利用しているケースもあります。

もちろん date-fns は日付計算にも利用できるため一概には言えませんが、「表示だけ」の用途であれば標準 API で完結できる可能性があります。

ロケール対応が自然

例えば、

new Intl.DateTimeFormat("ja-JP", {
  dateStyle: "long",
});

で日本語表示、

new Intl.DateTimeFormat("en-US", {
  dateStyle: "long",
});

で英語表示ができます。

SSR やサーバーサイドで扱うケースでも利用しやすそうです。

少し分かりづらいところ

一方で、使い始めて最初に感じたのは「フォーマットが分かりづらい」ということでした。

date-fns であれば、

format(date, "yyyy/MM/dd");

を見るだけで出力形式が分かります。

一方で、

new Intl.DateTimeFormat("ja-JP", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
});

は少し読みづらく感じます。

また、この設定を毎回書くのも少し面倒です。

フォーマッターを定義して使う

調べていて良さそうだと思ったのが、用途ごとにフォーマッターを定義してしまう方法です。

export const dateFormatter = new Intl.DateTimeFormat("ja-JP", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
});

export const dateTimeFormatter = new Intl.DateTimeFormat("ja-JP", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
});

export const monthDayFormatter = new Intl.DateTimeFormat("ja-JP", {
  month: "numeric",
  day: "numeric",
});

利用側は、

dateFormatter.format(article.publishedAt);

dateTimeFormatter.format(user.createdAt);

monthDayFormatter.format(eventDate);

のようになります。

この形であれば、

  • フォーマット定義を集約できる
  • 利用箇所の可読性を保てる
  • 表示形式の変更がしやすい

というメリットがあります。

利用側を見ると、「何の形式で表示しているか」が名前から分かるのも良いと感じました。

また、別途調べているうちに見かけましたが、 Intl.DateTimeFormat のインスタンスを沢山作るとロケール解決などの兼ね合いでメモリの消費が多いようです。今回の場合、フォーマット毎にインスタンスを作り各所で使いまわす想定になっているのでフォーマットが増えない限りはその問題も薄そうです。

日付計算はやはり date-fns が便利

ただし、日付計算まで Intl.DateTimeFormat で頑張る必要はないと思っています。

例えば、

addDays(date, 7);

startOfMonth(date);

endOfMonth(date);

のような処理は date-fns の方が圧倒的に分かりやすいです。

また、

isSameDay(date1, date2);

differenceInDays(date1, date2);

のような比較処理も date-fns が得意な領域です。

現時点での考え

まだ実運用では date-fns を利用しています。

ただ、あらためて調べてみると、日付表示だけであれば Intl.DateTimeFormat を選ぶ場面は十分ありそうだと感じました。

一方で、

  • 日付計算
  • 日付比較
  • 期間処理

まで含めると、引き続き date-fns の価値は大きいと思います。

そのため今のところは、

  • 表示 → Intl.DateTimeFormat
  • 計算・比較 → date-fns

という使い分けが有力そうです。

おわりに

これまで日付フォーマットというと、まず date-fns や dayjs を利用することが多くありました。

しかし現在では、ブラウザや Node.js のサポートも充実しており、表示だけであれば標準 API の選択肢も十分現実的です。

もしプロジェクトで日付ライブラリを利用している場合は、一度「これは表示だけなのか、それとも日付計算も必要なのか」を見直してみると、新しい選択肢が見えてくるかもしれません。