Moment.js → Day.js の移行は全置換じゃだめ。イミュータブルですので


タイトルで言いたいことがわかった人は読む必要のない話です。

サマリ

Day.js は JavaScript の軽量な日付操作ライブラリで、Moment.js のAPIと互換性があるため、Moment.jsからの移行先として候補にあがることが多いです。

Moment.js との大きな違いとして、単体で Locale の情報を含んでおらず、プラグインを追加して利用する必要があります。
これについてはこの記事では割愛します。

もうひとつ、Day.js は Moment.js と違って immutable(不変オブジェクト) であるという特徴を持ちます。
個人的にこれは Moment.js と比べて優れた点であると思いますが、移行の際は気をつけないと、発覚の遅れるバグを生む危険性があります。

日本語の紹介記事をざっとググった限り、この点にちゃんと言及している記事が見つからず、誰かが不幸になりそうな気がしたので書いておきます。

検証バージョン

  • Moment.js: 2.28.0
  • Day.js: 1.8.36

何が危険か

前述のとおり、Day.js は Moment.js となまじ互換性のある API を持つため、ソースコード内で moment と書いているところをガバっと dayjs に置換してしまえば、それでほとんど問題なく動いてしまいます。

実際そのような説明をしている日本語情報もいくらか見つかるのですが、 addsubtract といった操作関数の使い方によっては、意図しない結果を生むことがあります。

具体例

Moment.js を使い、日付に1日を加算してから文字列化するコードです。

const m = moment("2020-12-31");
m.add(1, "days");
console.log(m.format("YYYY/MM/DD"));

出力結果は「2021/01/01」となります。
これを Day.js に置き換えます。

const d = dayjs("2020-12-31");
d.add(1, "days");
console.log(d.format("YYYY/MM/DD"));

出力結果は 「2020/12/31」 となります。

解説

蛇足ながら解説しておきます。

Moment.js の add 関数は、戻り値で加算後のオブジェクトを返すと同時に、自身の値も書き換える挙動をします。

const m1 = moment("2020-12-31");
const m2 = m1.add(1, "days");
console.log(m1.format("YYYY/MM/DD"));   // 2021/01/01
console.log(m2.format("YYYY/MM/DD"));   // 2021/01/01
console.log(m1 === m2); // true(実は同じオブジェクト)

Moment.js 単体で見た場合、この挙動はしばしばバグを生む原因でもありました。
もっと凶悪なのは、 startOf という、副作用を持つことが想像しにくい関数さえも内部状態を書き換えてしまうことです。

const m1 = moment("2020-12-31");
const m2 = m1.startOf("month");
console.log(m1.format("YYYY/MM/DD"));   // 2020/12/01
console.log(m2.format("YYYY/MM/DD"));   // 2020/12/01
console.log(m1 === m2); // true

Day.js はこのような挙動を改善するため、これらの関数を内部状態を書き換えないイミュータブルな仕様に変更しています。
最初から Day.js を採用する場合は望ましい挙動ではありますが、Moment.js で動いていたものをそのまま置き換えられるわけではない点に注意が必要です。

const d1 = dayjs("2020-12-31");
const d2 = d1.startOf("month");
console.log(d1.format("YYYY/MM/DD"));   // 2020/12/31
console.log(d2.format("YYYY/MM/DD"));   // 2020/12/01
console.log(d1 === d2); // false

結び

以上です。