先日、面白いコードに出会いました。
知っている人からすれば「そんなことか」という内容ではあるものの、後学のために残しておこうと思います。

どんなコードだったか

以下のようなコードです。

const items = [
  { date: new Date(2025, 0, 28, 14, 0) }, // 2025-01-28 14:00
  { date: new Date(2025, 0, 30, 14, 0) }, // 2025-01-30 14:00
  { date: undefined },
  { date: new Date(2025, 0, 30, 12, 0) }, // 2025-01-30 12:00
  { date: new Date(2025, 0, 23, 14, 0) }, // 2025-01-23 14:00
];

items.sort((item1, item2) => (item1.date?.getTime() ?? 0 < (item2.date?.getTime() ?? 0) ? -1 : 1));

console.log(items);

何をしているのか

どうやら、配列を日付の新しい順に並び替えようとしています(sort 関数については MDN をご確認ください)。
getTime() は 1970/1/1 からの経過ミリ秒を返す関数なので、この値を比較することでソートしています。

注意点として、オブジェクト内の date プロパティは undefined になる可能性があります。
このオブジェクトについては、経過ミリ秒を 0 として判断することで後ろに並べようとしています。

上記を踏まえると、期待する出力結果は以下です。

[
  { date: 2025-01-30T05:00:00.000Z },
  { date: 2025-01-30T03:00:00.000Z },
  { date: 2025-01-28T05:00:00.000Z }
  { date: 2025-01-23T05:00:00.000Z },
  { date: undefined },
]

ところが、出力された結果は以下の通りでした。

[
  { date: 2025-01-23T05:00:00.000Z },
  { date: 2025-01-30T03:00:00.000Z },
  { date: undefined },
  { date: 2025-01-30T05:00:00.000Z },
  { date: 2025-01-28T05:00:00.000Z }
]

単純に並び順を反転しただけになってしまっています。

どう解決したのか

もうおわかりの方もいらっしゃると思いますが、ソート条件を修正しました。

// ソート用の関数を追加
function sortByDate(item1, item2) {
  const time1 = item1.date?.getTime() ?? 0;
  const time2 = item2.date?.getTime() ?? 0;
  // 今回のケースでは、同値は基本的にありえないため無視しても良かったが、
  // 一応 0 を返して並べ替えを行わないようにした
  if (time1 === time2) {
    return 0;
  }
  return time1 < time2 ? 1 : -1;
}

items.sort(sortByDate);

console.log(items);

これで、期待する結果を出力してくれるようになりました。

[
  { date: 2025-01-30T05:00:00.000Z },
  { date: 2025-01-30T03:00:00.000Z },
  { date: 2025-01-28T05:00:00.000Z },
  { date: 2025-01-23T05:00:00.000Z },
  { date: undefined }
]

何が起きていたのか

演算子が期待する順番で処理されていませんでした。

今回のコードには、以下3種類の演算子が利用されています。

  1. Null 合体演算子 (a ?? b)
  2. 小なり (a < b)
  3. 条件(三項)演算子 (a ? b : c)

MDN を見ると、上記3種類の演算子の優先順位は 2 > 1 > 3 となります。
ソート処理だけを抜き出すと、以下のようになります。

item1.date?.getTime() ?? 0 < (item2.date?.getTime() ?? 0) ? -1 : 1

期待する処理順は以下のとおりです。

  1. item1.date が undefined の場合は 0 を、それ以外の場合は item1.date の経過ミリ秒を利用する
  2. item2.date が undefined の場合は 0 を、それ以外の場合は item2.date の経過ミリ秒を利用する
  3. 上記 1 と 2 を比較し、1 < 2 の場合は -1 を、それ以外の場合は 1 を返す

前述の演算子の順番を考慮すると、小なりは Null 合体演算子より先に処理されます。
そのため item1.date?.getTime() ?? 0 よりも 0 < (item2.date.getTime() ?? 0) が先に計算されてしまっていました。
つまり、ソート処理としては以下のようなコードと同等です。

const a = 0 < (item2.date.getTime() ?? 0);
return (item1.date?.getTime() ?? a) ? -1 : 1;

結果的に、item1.date または item2.date が undefined でなければソート処理は常に -1 を返すため、今回のような問題が起きたというわけです。

まとめ

今回の問題、テストコードがあれば気づけた可能性は高いです。
また、1行で色々やるとかえってややこしくなるという典型例だったようにも思います。

改めて、自分も気をつけようと思います。