【今日の知見】react-simple-typewriterで複数行にまたがる文字送りエフェクトを簡単に作る

急にタイプライター風エフェクト(1文字ずつ表示されるやつ)が作りたくなったので、react-simple-typewriterを導入した。

const MyComponent = () => {
  return (
    <div style={{ margin: "1rem" }}>
      <h3>夏目漱石『草枕』より</h3>
      <Typewriter
        words={[
          "智に働けば角が立つ。情に棹させば流される。意地を通せば窮屈だ。とかくに人の世は住みにくい。",
        ]}
      />
    </div>
  );
};

こういうコードを書くだけで、かんたんにゲーム風?通信風?の表示ができる。

カーソルの表示/非表示や表示速度のコントロールも簡単でめちゃくちゃ助かるのだが、改行付きテキストを入れても半角スペースに置換されて複数行表示ができないという問題があってかなり困っていた。しょうがないから自作するか…と思っていたが、ググったらすぐ解決した。

解決策は簡単で、react-simple-typewriterは親要素からスタイルを受け継ぐ。親要素側で改行コードを半角スペースに置換しないスタイルwhite-space:pre-lineを設定してやればよいのだった。 white-space - CSS: カスケーディングスタイルシート | MDN

const MyComponent = () => {
  return (
    <div style={{ margin: "1rem", }}>
      <h3>夏目漱石『草枕』より</h3>
      <Typewriter
        words={[
          "智に働けば角が立つ。情に棹させば流される。\n意地を通せば窮屈だ。とかくに人の世は住みにくい。",
        ]}
      />
    </div>
  );
};

react-simple-typewriterRPGっぽい挙動をさせたり、ChatGPTっぽい挙動をさせるのにも向いている。簡単に使えて色々設定できる(すべて表示されたときに特定の関数を実行したりできる)ので、存在を知っておくと困った時の助けになってくれそうだ。

1904年になりました(dayjsでの年入力の話)

この記事ははてなエンジニア Advent Calendar 2023 - Hatena Developer Blogの1/1の記事です。


「2024」と打ち込んだはずなのに……

1904年になってしまいました。今年もよろしくお願いします。id:nakatakiです。

辰年生まれなので年男…と言いたいところですが、どうやら年表示が壊れてしまっているようです。この原因はdayjsの仕様の穴にありました。一緒に原因を探してみましょう。

以下のようなコンポーネントがありました。(重要な部分だけをまとめた仮のものです)

const DateInput: React.FC = () => {
  const [selectedDate, setSelectedDate] = useState("");

  const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const inputDate = event.target.value;
    const parsedDate = dayjs(inputDate, "YYYY-MM-DD");
    if (parsedDate.isValid()) {
      // 本来は色々処理する
      setSelectedDate(parsedDate.format("YYYY-MM-DD"));
    } else {
      // invalidなら今日の日付にしておく
      setSelectedDate(dayjs().format("YYYY-MM-DD"));
    }
  };

  return (
    <input
      type="date"
      max="9999-12-31"
      value={selectedDate}
      onChange={onChange}
    />
  );
};

これは、dayjsという日時処理ライブラリを利用した、年月日を入力するコンポーネントです。似た機能を持つMoment.jsが新規開発を停止したので、乗り換える…という方も多いと思います。 このような表示になります。

ここでは、様々な事情により、dayjsによるバリデーションを経由したデータをもう一度inputに戻しています。

この状態で一度日付を選択し、キーボードで試しに「1234」と打ち込んでみると…

なんだこれ!

「1901年→1902年→1903年→1904年」と画面が遷移してしまっています。何を入力してもこの調子。 だから「2024」を入力しても「1904」になってしまったんですね。

原因

dayjsのコーナーケース

こうなっている原因は、現在のdayjsのコーナーケースにあります。 こちらをご覧ください。(https://jstool.gitlab.io/dayjs/ を使用しています)

この画像が示すように、現在のdayjsは、'0001-01-01'から'0099-12-31'までの文字列を1900年代として解釈します

JSのDateのコンストラクタで、Yに2桁の数を渡すと1900年台として解釈されるのはそこそこ有名かと思いますが、dayjsの内部でこれを呼び出しているため同様の現象が起きているようでした。

Issueにもなっていますが、現在は仕様として扱われているようです。

Dateとは違って"YYYY-MM-DD" で渡しているんだから1900年代じゃないのは明白じゃないか、とは思うのですが…。

仕組み

仕様がわかればあとは簡単です。先ほどの「1234」の例で、なぜこんな挙動になったか順を追って考えてみます。

  1. キーボードで「1」を押した時点で、inputの中身は'0001-01-01'になっています。
  2. dayjsはこれを1900年1月1日と解釈し、seledtedDate1900-01-01がセットされます。
  3. inputの中身が'1901-01-01'に置き換わってしまうため、フォーカスが下1桁に戻ります。
  4. 次にキーボードで「2」を押すと、inputの中身は'0002-01-01'になります。(2に戻る)

なるほどこれでは、いつまで経っても2024が入力できませんね。

解決

dayjsにStringを渡していてはきりがありません。一旦Date.parseを使ってエポック秒にしてから渡しましょう。

const parsedDate = dayjs(Date.parse(inputDate));

すると…

正常に動作し、無事に2024年になることができました! これで干支も辰年、晴れて年男に…ってあれ?

1904年も辰年だったのか。僕は2000年生まれだから…マイナス96歳って年男になりますか?

まとめ

  • input type="date"の中身をdayjsで逐一パースしていると、キーボード入力がおかしくなるぞ(Moment.jsはこうはならない)
  • dayjsに'0001'年から'0099'年を扱って欲しい時は、StringではなくEpochで渡すと良さそう

TypeScriptでユニオン型をオブジェクトのキーにしたい時はMapped Typesが良さそう(12/27 今日の知見)

今日の知見

TSでEnumを使いたくない時に、それっぽくUnion型を使うことがある。

type Fruits = "りんご"|"みかん"|"バナナ";

そんな時に、こういう制約↓をつけたオブジェクトを定義したいとする。

これならOKだが…

type FruitsPrice = {
  [key in Fruits]: Number 
} // OK

こっちはコンパイルエラーになる。

interface FruitsPrice {
  [key in Fruits]: Number;
} // NG
  • できればinterfaceで書きたいわけだが…今回はそうもいかない!という話

[key in Fruits]の書き方はMapped Typesと呼ばれるもので、現在interfaceには対応できない。( https://typescriptbook.jp/reference/type-reuse/mapped-types)

Mapped Typesは追加のプロパティが書けないなど、安心ではあるけど融通は効かない。Mapped Typesで必ずしも要望が叶うとは限らなそうだ

まとめ

  • 自作のユニオン型をオブジェクトのキーに指定したいときは、Mapped Typesを使うのが望ましい
    • しかし、制約はある
      • interfaceでは使えない
      • 例えば型にないキーを追加したいときは困る
        • …これ本当にどうしたらいいんだろう?解決策あるのかな?
    • だが、情熱はあるみたいに言うな

食洗機のサブスクを始めた

Panasonicの食洗機「SOLOTA」のサブスクを契約してみた。届くのは多分来月以降。

37000円くらいのやつを月1290円で借りられて、36回払ったら所有権がこっちになる。

一応分割払いのシミュレーションしてみたらなんならカード払いより総支払額が安かったし、仮に引越しをして手放さなきゃいけなくなった時の保険としていつでも離脱できるのはすごくありがたい。スマホなら下取りとかメルカリに出したりで買いきったほうがいいけど、食洗機はリセールバリューなんて考えないからね…。