📄
概念 📚 software-design-concepts

型設計とコントラクト

型システムを活用した不正状態の排除・ブランド型・型による不変条件の表現とDesign by Contract

型設計とは、型システムを使って「不正な状態をコンパイル時に表現不可能にする」アプローチだ。「Make Illegal States Unrepresentable」というフレーズはScalaやF#コミュニティで広まり、TypeScriptにおいても強力に適用できる。バリデーションをランタイムのif文で行うのではなく、型そのものに制約を埋め込むことで、関数のシグネチャを見るだけで前提条件が分かるコードになる。

Branded Types(ブランド型)はプリミティブ型に名前上の区別を付ける手法だ。string型のUserIdProductIdは構造的には同じだが、誤った引数渡しをコンパイル時に防ぎたい場合に有効だ。TypeScriptの構造的部分型では type UserId = string & { readonly _brand: 'UserId' } のように実装する。Union Typesは状態を直和として表現し、各ケースに必要なフィールドのみを持たせることで、ありえない状態の組み合わせをそもそも型に存在させない。

Design by Contract(契約による設計)はBertrand Meyerが提唱した概念で、関数の事前条件(precondition)・事後条件(postcondition)・不変条件(invariant)を明示する。TypeScriptでは型レベルで表現できるものはそうし、ランタイムレベルの契約はZodやio-ts・Effectで実装する。特にシステム境界(APIレスポンス・フォーム入力・外部JSON)では必ずランタイムバリデーションを行うべきだ。

コードレビューで着目するポイント

  • stringnumberで表現されているIDや金額がBranded Typesに置き換えられるか
  • null/undefinedの可能性がある値がOptional型として適切に表現されているか
  • Union Typeで表現できる状態遷移をbooleanフラグの組み合わせで表現していないか
  • 関数が受け入れる型の範囲と実際に使える範囲に乖離がないか(事前条件の明示)
  • Zodスキーマが型定義と一致して管理されているか(型とバリデーションの二重管理の回避)
  • asキャストによる型の強制変換がバリデーションなしに行われていないか
  • ドメインオブジェクトがプリミティブ型ではなく意味ある型で表現されているか

典型的なアンチパターン

フラグ地獄: isLoading: boolean, isError: boolean, isSuccess: boolean を同時に持つオブジェクト。この3フラグは loading | error | success のUnion Typeで表現すべき状態であり、isLoading=true, isSuccess=true という不正な状態がコンパイルレベルで存在してしまう。

any乱用: 型推論が難しい箇所で即座にanyを使う。型の安全性が局所的に崩壊し、型エラーが境界を越えて伝播する。unknownを使って型ガードで絞り込む方が安全だ。

ランタイムバリデーション未実施: APIレスポンスをZodなどで検証せずに型アサーションで処理する。外部データの変化に対して脆弱であり、型が正しくてもランタイムエラーが発生する。

参考リソース