適切な問題の切り分け無き安易な「共通化」は片手落ち
「共通してる部分を見つけたから共通化する!」ではなくて、
「『共通』と向き合った結果…抽象化することによってコード全体の複雑性をグッと下げてくれる勘所を見つけた!」というときに、良いユーティリティが生まれる
安易だが有益じゃない「共通化」によるユーティリティの例:
code:typescript
/**
* @param date YYYY-MM-DD (ISO 8601 拡張形式)
* @return date が不正な場合、空文字列を返す
*/
const formatDate = (date: string, format: string): string =>
サーバーから受け取った YYYY-MM-DD 形式の日付をパースしてから一定のフォーマットに整形するユーティリティ
起こりそうなこと
「date が不正だった」という事実が、型の上では握りつぶされてしまうので、ロジックの見通しが悪い
中で date-fns とかを使って date をパースしているので、 例えば、サーバーが誤ってタイムゾーンを指定した日付を送ってきても、たまたまエラーを起こさず結果だけがズレてしまう
「なぜか動く」は怖いぞ
やっぱり、別のページでは、タイムゾーン情報も含む日時の文字列を扱うことになった…
でも、formatDate って名前で、タイムゾーン無し用のユーティリティがあるぞ?
formatDate を拡張して引数を増やすか?→論理的凝集地獄
新しい関数を作るか?formatZonedDateTime→名前がややこしい。 無標な名前を安易に使うな!
パースとフォーマットは分けるべきでは?
パースは入口で行って、フォーマットは出口と離してあげると、その間では「おかしなこと」が起こらないと型レベルで保証できる
ただし、JS の Date はダメダメなので、これができない
(パースの失敗を Invalid Date なるオブジェクトで表して握りつぶされているので、型レベルで失敗が隠れてしまう)
良い「依存先」を利用して、有益になった利用側コードの例:
↓ これは思いつき & 抽象的イメージ。もうちょっと本腰入れて考えないと良い感じのが思いつかない。
思いついたら Qiita にアウトプットするか…
良い「依存先」
日付は Temporal (今は polifyll が要るけど…)
バリデーション・パースの「ロジックの構造」は standard schema のインターフェースを利用する
code:typescript
import * as v from 'valibot';
/**
* サーバーから受け取った日付文字列を Temporal.PlainDate として使用可能にするスキーマ。
*
* YYYY-MM-DD 形式(ISO 8601 Calender Date)のみが有効。そうでない入力は無効。
*/
export const plainDateFromServerSchema = v.pipe(
v.string(),
v.rawTransform(({ dataset, addIssue, NEVER }) => {
// valibot の書き方がまだわからないので一旦省略
}),
)
code:typescript
import * as R from "remeda";
import { parse, unwrapResultOrUndef } from "utils/standardSchema";
// データ源に近いコード
const plainDate = R.pipe(
plainDateFromServer,
parse(plainDateFromServerSchema),
unwrapResultOrUndef
);
// 表示側コード
// 中略
releaseDateFormatter(plainDate)