/note/tech

制約を表現する型を書く

要約:

■ 1. 型の基本的な役割

  • 型 = プログラムの制約を表現する手段
  • 型がない場合、どんな値が渡されるか分からず、防御的なプログラミングが必要になる
  • 型がない場合の対処手段は命名規則・コメントなどナイーブなものに限られる

■ 2. 型による事前条件の表明

  • 型を使って関数の引数の想定範囲を狭め、関数本来の目的にフォーカスできる
  • 型があることで処理内容を読まずとも関数の中身が想像できる(意図の明白なインターフェース)
  • 実装内部のバリデーションによる制約(例: 配列の長さ1以上)はインターフェースに現れない

■ 3. NonEmptyArrayによる制約の型表現

  • type NonEmptyArray<T> = [T, ...T[]] により、1つ以上の要素が必ず存在する配列型を定義できる
  • スマートコンストラクタ(ファクトリ関数)を定義することでNonEmptyArrayを安全に生成する
  • NonEmptyArray<Product> を引数の型とすることで、空配列の排除を型レベルで表明し、バリデーションが不要になる

■ 4. 全域関数と部分関数

  • 全域関数: 任意の入力について答えが定まる関数(必ずreturnする)
  • 部分関数: 答えが定まらない入力が存在する関数(Errorが出る入力パターンがある)
  • 例として、足し算はオーバーフローを考慮しなければ全域関数、割り算はN÷0が定義されないため部分関数
  • 定義域(引数の取りうる値の範囲 = 型)を狭めることで、部分関数を全域関数にできる

■ 5. 全域関数が有益な理由

  • 全ての情報が型に表出するため(throwされたエラーは型に表出しない)
  • 型に表出することでTypeScriptの型システムの恩恵を受け、より堅牢な静的解析が可能になる
  • 定義域を狭める方法と対になるアプローチとして、値域(戻り値の範囲 = 型)を広げるResult型がある

■ 6. Result型

  • 本来であればErrorをthrowするところを、値として返すアプローチ
  • 入力の範囲を狭めるだけではエラーを防げない(副作用がある)場合に有効
  • エラーパターンがインターフェースに表出するという点は定義域を狭める方法と同様

■ 7. 純粋なTypeScriptの型表現の限界

  • NonEmptyArrayのように素直に定義できる制約型は稀
  • 空文字列を許容したくないケース:
    • 純粋なTypeScriptでは「1文字以上の文字列」を表現できない
  • 特定のドメインモデルを指定したいケース:
    • 純粋なTypeScriptでは構造的部分型の性質により、パース前のデータとパース後のドメインモデルを区別できない

■ 8. Branded Typesによる解決

  • 純粋なTypeScriptで表現できない制約はBranded Typesで解決できる
  • TSKaigi 2026 Leveragesトラック(DAY2 15:10〜15:40)にて詳細を解説予定