Closed19

moment.js を temporal に移植するのはどのくらいラクか or 大変か

f_subalf_subal

実は筆者はすでに一度(2020年8月) temporal にフィードバックのアンケートを送ったことがあるが、そのときは新規プロジェクトでの使用を想定したもので、既存のを移行するとどのくらいつらいかはあまり考えていなかった。

また、今 temporal-polyfill を見たら当時からさらに API が変わっていた。
https://www.npmjs.com/package/proposal-temporal

具体的には、現在時刻をタイムゾーン指定で生成するためのメソッドである Temporal.now.zonedDateTimeISO() は私がアンケートを送った頃にはなかったし、Duration に負の値を取れるというのも当時は確定してなかった。

https://github.com/tc39/proposal-temporal/blob/54b36341d9cd9b202938354f09964ee4a66fa349/meetings/agenda-minutes-2020-06-11.md#608-negative-durations

f_subalf_subal

結論、当然のことではあるが、moment.js からの移行コストだけを考えるのであれば day.js の方が圧倒的に楽でおすすめできる。

https://github.com/iamkun/dayjs

moment → day.js と違い、moment → temporal は単なる API の変更以上に圧倒的に思想の変更を余儀なくされる。

f_subalf_subal

ちなみに、以下の検証をする前は私は temporal に全面移行する気満々だった( 数ヶ月前の自分は、「どうせすぐ temporal 来るんだから急いで date-fns や day.js に移行したってもう一回捨てるだけだよ」とか言ってた )

f_subalf_subal

例) フォーマット。item.createdAt は ISO8601 文字列とする( 2020-11-29T12:00:00

// moment.js

<div>
  {moment(item.createdAt).format('YYYY年MM月DD日')}
</div>
// day.js
<div>
  {dayjs(item.createdAt).format('YYYY年MM月DD日')}
</div>
// temporal

<div>
  {Temporal.PlainDate.from(item.createdAt).toLocaleString('ja-JP', {
    year: 'numeric',
    month: 'long',
    day: '2-digit',
  })}
</div>

temporal 長い……長いが i18n 対応を明確に意識できる点は良い( option は Intl.DateTimeFormat と同じものが渡せる )

f_subalf_subal

たぶん、toLocaleString を毎回呼ぶのがダルさの原因で、Intl.DateTimeFormat のインスタンスを使い回すほうがマシかも。

export const yyyyMdDd = new Intl.DateTimeFormat('ja-JP', {
  year: 'numeric',
  month: 'long',
  day: '2-digit',
})

const createdAt = Temporal.PlainDate.from(item.createdAt)

yyyyMdDd.format(createdAt as any)

が、Intl.DateTimeFormat['format'] の型定義はそのままでは temporal のインスタンスを受け付けないので型定義をいじっておく必要がある

f_subalf_subal

そもそも現状の Intl.DateTimeFormat は temporal のオブジェクトを受け取れるのか?

f_subalf_subal

temporal 使った時の感想として、そもそも暦やタイムゾーンを意識しないと手が進まないタイプの API だなーというのがあった。

これは慣れのせいもあるかもしれない。
デフォルトではタイムゾーンを意識しなくても使える moment.js / day.js と比較したとき、強制的に意識させられたほうが健全なケースも多いはず。

が、元々意識せずに書かれたコードをリファクタリングして temporal に移行しようと思うと脳の負荷がすごい
(「えーとここは unix ミリ秒なので Temporal.Instant だが、こっちは Temporal.PlainDate で作って良いんだっけ」)

f_subalf_subal

たとえば Storybook の addon で date() knob を使い、ISO8601 文字列を受け取るコンポーネントの Story を書いているとしよう。

// moment.js
export const 通常時 = () => {
  const createdAt = date('作成日時', new Date())

  return <TargetComponent createdAt={moment(createdAt).toISOString()} />
}
// temporal
export const 通常時 = () => {
  const createdAt = date('作成日時', new Date())
  const createdAtIso8601 = Temporal.Instant.fromEpochMilliseconds(createdAt)
    .toZonedDateTimeISO({ timeZone: Temporal.now.timeZone() })
    .toString()

  return <TargetComponent createdAt={createdAtIso8601} />
}
f_subalf_subal

だるすぎるので、temporal を使うなら自分で date() をラップして専用の knob を作るだろう

export const iso8601 = (label: string, legacyDate = new Date()) => {
  return Temporal.Instant.fromEpochMilliseconds(date(label, legacyDate))
    .toZonedDateTimeISO({ timeZone: Temporal.now.timeZone() })
    .toString() as ISO8601
}
export const 通常時 = () => {
  const createdAt = iso8601('作成日時', new Date())
f_subalf_subal

Storybook 6 組み込みの Controls でも同じ問題が発生するだろうか?こっちは検証していない

petamorikenpetamoriken

それだと文字列に Zone が含まれてしまうので

Temporal.Instant.fromEpochMilliseconds(date(label, legacyDate))
    .toString({ timeZone: Temporal.now.timeZone() }) as ISO8601

かと。

f_subalf_subal

総じて、「元のコードがどういうタイムゾーンや i18n などを意識して書かれてたかを読み解きながら temporal に置き換える」のはとてもつらい。

一方新規で書く時に temporal ファーストで設計するとすごく健全なコードがかけそう。

プロジェクト内の moment.js をサクッと捨てることが目的ならおとなしく day.js に逃げたほうが良いのでは…

f_subalf_subal

逆に、temporal だからできる圧倒的に素晴らしい点として、型の豊富さがある。

たとえば temporal には PlainYearMonth 型( 「2020年の11月全体」を表すインスタンスが作れる )や PlainMonthDay 型( 「どの年にも紐付かない11月30日」を表すインスタンスが作れる )がある。

単に「今月」という情報が欲しかっただけの際に、現在時刻を M で format と書かなくていいというのはセマンティクスとして美しい。

f_subalf_subal

それこそ「キャラクターの誕生日」を扱うアプリケーションを作るのに PlainMonthDay 型にマッピングしてロジックを書けたらかなりキレイだと思う

このスクラップは2020/12/27にクローズされました