Option<NonEmptyString>とstringとstring | nullを区別しよう
こちらは Optimind Advent Calendar 2025 の 8日目の記事です。
半分アイデア記事みたいな軽めのやつです。
テキストフィールドの値、どう扱いますか
┌─────────────────────────────────────────────────┐
│ 住所 │
├─────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ 東京都渋谷区神宮前1-2-3 │ │
│ └───────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
テキストフィールド、特にオプショナル (空白のまま進めることができる) ようなテキストフィールドを扱う上で、そこに入力された値をどう扱いますか、という話です。
素朴にやれば、 string になるかと思います。
そこで、たとえば住所が入力されている場合のみ、住所とグーグルマップへのリンクを表示する、というようなUIだったとしましょう。
住所が入力されている場合:
┌─────────────────────────────────────────────────┐
│ │
│ 📍 東京都渋谷区神宮前1-2-3 │
│  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ │
│ 🔗 Googleマップで見る │
│ │
└─────────────────────────────────────────────────┘
住所が空の場合:
┌─────────────────────────────────────────────────┐
│ │
│ (何も表示されない) │
│ │
└─────────────────────────────────────────────────┘
このような場合、空文字列に対する判定が必要になりますね。
const address: string = use(/* ... */);
return (
<div>
{address !== "" && (
<div>
<p>📍 {address}</p>
<a href={`https://maps.google.com/?q=${address}`}>
🔗 Googleマップで見る
</a>
</div>
)}
</div>
);
これがなんか、気持ちわるいですね、という話が前提になります。これは前提して進めますね。
そうすると、 string | null としてやる方法が考えられますね。
const address: string | null = use(/* ... */);
return (
<div>
{address !== null && (
<div>
<p>📍 {address}</p>
<a href={`https://maps.google.com/?q=${address}`}>
🔗 Googleマップで見る
</a>
</div>
)}
</div>
);
さらに、文字列は空文字列であることはないので、そのような文字列を表す型を NonEmptyString として、また、ついでにnullableの表現も Option<..> として (これは便利な T | null ぐらいの気持ちなので等価で考えてもらってもよい) 以下のように書けます。
const address: Option.Option<NonEmptyString> = use(/* ... */);
return (
<div>
{address.pipe(
Option.match({
onNone: () => null,
onSome: (value) => (
<div>
<p>📍 {value}</p>
<a href={`https://maps.google.com/?q=${value}`}>
🔗 Googleマップで見る
</a>
</div>
),
})
)}
</div>
);
ちなみに Effect.ts を想定した書き方になっています。
さて、 Option.Option<NonEmptyString> にしたいとして、我々はどこかからそれを取ってこなければならず、その都合も考えねばなりません。
それが次の話になります。
それぞれの表現・エンコード・デコード
string と表現できる範囲は Option<NonEmptyString> は変わっていません。ではなぜ、これが必要なのでしょうか。
これを考えるには、逆にそもそも、誰の都合で string という形式が必要なのかを考えます。そうすると、これはいくつかの地点での表現形式とまとめられます。
-
<input>などが提供する、その値を取り扱う方法はstring - メジャーなライブラリが提供する
<input>のラッパーも大抵はstring - (ユーザー、また、プレゼンテーションで)入力/表示していると認知している値の対象は
Option<NonEmptyString> - APIで返すときは、JSON APIであれば、JSONで表現可能な範囲
- データベースで保存するときは text や varchar で、DBMSや設定にもよるが、 Unicode文字列、など (より細かくいえばヌル文字を含まない、など)
- データベースの値をORMなどで認識するときは
string
このように、永続化や、JSONでのやりとり、ユーザーの認知等の境界を越える上であくまでも表現を越えなければならないだけで、一貫としてその中心には Option<NonEmptyString> だという気持ちになることができます。その都合のための表現の切替えをエンコード・デコードと呼ぶわけですね。
図にまとめると以下のようになります。
┌───────────────────────────────────────────────────────┐
│ │
│ ユーザーの認知 │
│ 「住所を入力した」/「住所を入力していない」 │
│ │ │
│ │ 入力していないなら空のままにする │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ <input> / フォームライブラリ │ │
│ │ string │ │
│ └───────────────────────────────────────────────┘ │
│ │ │
│ │ encode: Option.getOrElse(() => "") │
│ │ decode: s !== "" ? Option.some(s) │
│ │ : Option.none() │
│ ▼ │
│ アプリケーション状態 ──────▶ プレゼンテーション │
│ Option<NonEmptyString> (上記コード) │
│ ▲ │
│ │ Schema.encode(ApiSchema) │
│ │ Schema.decode(ApiSchema) │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ API (JSON) │ │
│ │ │ │
│ │ (JSONで表現できるなんらか) │ │
│ └────────────────────────────────────────────┘ │
│ ▲ │
│ │ Schema.encode(ApiSchema) │
│ │ Schema.decode(ApiSchema) │
│ ▼ │
│ サーバー側アプリケーション │
│ Option<NonEmptyString> │
│ ▲ │
│ │ encode: Option.getOrElse(() => "") │
│ │ decode: s !== "" ? Option.some(s) │
│ │ : Option.none() │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ データベース (text / varchar) │ │
│ │ │ │
│ │ string │ │
│ └──────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────┘
ここで、 <input> やUIライブラリにさらにラッパーをかけて、 onInput が Option<NonEmptyString> としての変化を教えてくれる <OptionNonEmptyStringInput> を作ることも考えられそうですね。私のプロジェクトでもそうしたことを実際にしています。
この心持ちが大事
とはいえ小規模だったり、簡易的なプロジェクトでこれを徹底してやるのは大変かもしれません。重要なのはこの根底の気持ちをもって書くことかなと思います。私もサクっと作るときにここまではしませんね。
型プログラマの書く(型のない)スクリプト言語のプログラムがよりよい設計になる、みたいなそういう話です。
ちなみにアスキーアートの図だけClaudeCodeにお願いしました。個人的に一番よい使い方です。(文章は全部自分で書きたい)
mermaidでも良かったかもしれません。
世界のラストワンマイルを最適化する、OPTIMINDのテックブログです。「どの車両が、どの訪問先を、どの順に、どういうルートで回ると最適か」というラストワンマイルの配車最適化サービス、Loogiaを展開しています。recruit.optimind.tech/
Discussion