🎄

文字列や配列の最大長が決まっていないときの対策 / TypeScript一人カレンダー

2024/12/17に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2024の4日目です。昨日は『応用編 Valid branded types』を紹介しました。

スキーマ記述と不明確な上限値の問題

前回までの記事ではValibotを使った型定義とランタイムバリデーションの一元化や、Branded typesの応用について紹介してきました。こうしたアプローチでは、TypeScriptの型システム上で明確な制約を提示するために、string()number()pipe()brand()、そしてminLength(), maxLength(), minValue(), maxValue(), integer()といった多くのActionを活用します。

しかし、実際の業務開発では、最大値が定まっていないケースがしばしば登場します。

たとえば、文字列の場合、最小文字数を1文字以上と決めたとしても、最大文字数がまだはっきりと決まっていないことがあります。また、数値では文字列と同じように下限値が明確である一方で、上限は特に設定していないケースや、小数点や負数の扱いをどうするのか未決定のままという状況が珍しくありません。配列についても同様で、最小要素数は定めているものの、最大要素数はいつまでも決めないまま開発を進めてしまうことがあります。

こういった不明確な上限を抱える場合、スキーマを読む開発者は「このスキーマは単に記述漏れなのか、それともあえて上限を定めていないのか?」と迷うことになります。コードコメントで「最大は未定」と書いても良いですが、同じようなコメントがコード内に毎回散らばると読む側も煩雑ですし、コードコメントの微妙な表記揺れがプロジェクト内に散らばると、徐々にメンテナンス性を低下させていきます。

業務上「最大値未定」が頻出する理由

実務では、最小値や最小要素数など、何らかの「下限」については比較的早い段階で合意が得られることが多いものの、最大値や最大長に関しては後回しにされがちです。その理由は多岐にわたります。

たとえば、ユーザーニーズが明確でない段階では、低めの上限を設けてしまうと利用時における潜在的な機会損失を招く可能性があり、あえて上限を決めずに柔軟性を確保しようとすることがあります。また、上限はパフォーマンスやストレージコストの問題が顕在化した時点で設定すればよいと考える場合もあります。さらに、そもそも最大値を設定するという発想自体がなかった、というシンプルな理由も意外とよくみかけます。

このような状況だと、スキーマ作成時点で最大値を定義せず、実行時に問題があれば後で上限を導入するというプロセスがしばしば選択されます。しかし、だからといって何もコードやコメントのフォローがないと「考慮が漏れている」のか「後回しの判断が下されている」のかは開発者としては判別できません。意思決定の経緯がコードから透けてこないと、コードの差分を何度も遡ったり、PRを何個も読んで回ったり、Slackの過去ログをひたすら検索したりして、なぜこうなっているのかが判然としないまま開発の時間が奪われがちになるからです。

「最大値未定」を明示するアプローチ

筆者は、こういった場面に対処するため、Valibot用のカスタムアクションとしてmaxStringLengthNotSpecified()maxArrayLengthNotSpecified()といった関数を用意しました。これらは、最大長があえて未定であることをスキーマ上で明確にするための「記述漏れ防止用マーカー」のようなものです。

次のコードは、文字列の最大長が決まっていないことを示すカスタムAction maxStringLengthNotSpecified()です。

import { type LengthInput, type MaxLengthAction, maxLength } from "valibot";

const stringMaxLength = Number.MAX_SAFE_INTEGER;

export function maxStringLengthNotSpecified<
  TInput extends LengthInput,
>(): MaxLengthAction<TInput, typeof stringMaxLength, undefined> {
  return maxLength<TInput, typeof stringMaxLength, undefined>(
    stringMaxLength,
    undefined,
  );
}

続いて、次のコードは、配列の最大長が決まっていないことを示すカスタムAction maxArrayLengthNotSpecified()です。

import { type LengthInput, type MaxLengthAction, maxLength } from "valibot";

const arrayConstructorMaxLength = 2 ** 32 - 1;

export function maxArrayLengthNotSpecified<
  TInput extends LengthInput,
>(): MaxLengthAction<TInput, typeof arrayConstructorMaxLength, undefined> {
  return maxLength<TInput, typeof arrayConstructorMaxLength, undefined>(
    arrayConstructorMaxLength,
    undefined,
  );
}

これらは次のように使用します。このコードは業務上には存在しない本稿での架空のものです。

// なんらかのカテゴリ名
const categoryName = pipe(string(), minLength(0), maxStringLengthNotSpecified());

// 選択可能なオプションは最低1個
const options = pipe(array(string()), minLength(1), maxArrayLengthNotSpecified());

ValibotのカスタムActionですので、このように、pipe()に渡すことで使用できます。

なぜECMAScriptの限界値を参考にするのか

業務上の実際のコードは、前節のコードスニペットだけでなくこの関数が何者なのかというJSDocも一緒に書かれています。そのDocの内容を要約してご紹介します。

「上限を定めない」と言っても、実際にはECMAScriptの仕様上の上限、あるいは処理系の物理的な限界が存在します。たとえば、文字列の最大長はECMAScript仕様上、2 ** 53 - 1、つまりNumber.MAX_SAFE_INTEGERが上限とされています。

配列の場合は、2 ** 32 - 1という最大長が仕様によって定義されています。

これらはあくまでも仕様上の限界値であり極めて大きな数値であるため、実務上そこまでの長さや大きさに達することはまずありません。ですが、Number.POSITIVE_INFINITYを用いた「無限」を表すよりも、言語仕様上の明確な上限値を参照することで「本来は上限未定だが、言語仕様上こういう限界がある」という意図をコード上で表現することも可能です。

このような方針をどう捉えるかについては、正直なところ議論の余地があります。「最大値未定なら単にコメントでそう書けばよい」というもっともな意見もあれば、「無限の表現をしたいのであればInfinityで済むではないか」という考え方も成り立ちます。また「ユーザーニーズが決まった時点で上限値を設定すればいいので、今は空欄のまま放置しておけばよい」という意見も十分あり得るでしょう。

しかし筆者としては、maxStringLengthNotSpecified()maxArrayLengthNotSpecified()のような命名の関数を使うことによって、スキーマを読む人に「ここは意図的に未定としている。決して書き忘れではない」というメッセージを伝えられる点に価値を感じています。また、その実装は曖昧なままにせず「より小さな業務上の上限値を決めなかった結果、それはECMAScript仕様上の限界値によって束縛された」という表明に繋がると考えています。あくまでもこの考え方は一例でしかありませんが、コードはコミュニケーション手段であり、こうした自作ユーティリティは「開発者同士の合意形成」の一助となると信じています。

また、こういった措置を取ることで単にコメントで「未定」と書くだけよりも、ユーティリティ関数として存在する方が、論理的な依存箇所の検出が高速であるというメリットもあります。コードコメントは、大半のコードレビューでささっと読み飛ばされ、微妙な表記揺れ、コメント記述者による微妙な言い回しの違いの吸収に目が向かず、あとあとになってそのコメントがつけられた箇所の全検出が困難になるケースも少なくありません。関数であればエディタを使って使用箇所を一覧表示するだけで済みます。

まとめ

今回紹介したmaxStringLengthNotSpecified()maxArrayLengthNotSpecified()は、ちょっとマニアックなTipsです。真似する必要はなく、「そういう考え方もあるんだな」と思っていただければ嬉しいです。大切なのは、スキーマの読み手が解釈に迷わないようにするためにはどうすればよいか、工夫を凝らすこと。その配慮の仕方として「未定を明示するための関数」を導入するアプローチもあるというご紹介でした。これは開発チームやプロジェクト文化に合わせて最適なやり方を選べば良いでしょう。

明日は『実例 neverUsed, $』

本日は「文字列や配列の最大長が決まっていないときの対策」を紹介しました。明日もValibot利用の工夫としてneverUsed(), $()をご紹介します。それではまた。

Discussion