型システム
型は、値が取り得る状態と境界の契約を表すために使います。コンパイル時の型だけで外部入力の安全性を保証したことにはしません。
型の定義
オブジェクト、Union、交差型を一貫して扱えるため、アプリケーションコードではtypeを基本とします。宣言マージが必要な外部ライブラリとの統合など、interface固有の性質が必要な場合だけinterfaceを使います。
状態によって必要な値が変わる場合は、オプショナルプロパティを増やすよりDiscriminated Unionで表します。
type ImportState =
| { status: "idle" }
| { status: "running"; startedAt: Date }
| { status: "failed"; error: ImportError }
| { status: "completed"; importedCount: number }
switchではneverを使って網羅性を検査し、状態追加時の修正漏れをコンパイルエラーにします。
信頼できない値
HTTP、環境変数、ストレージ、データベース、外部APIから来た値はunknownとして受け取り、境界で検証します。anyで検査を回避しません。
実行時検証が必要な契約では、Zodなどのスキーマを正本にして型を推論します。一度検証した内部値を各層で繰り返しparseしません。RPCクライアントやデータベースから型が生成される場合も、同じ型を手書きで複製せず生成元を正本にします。
型アサーション
まず型の絞り込み、スキーマ検証、satisfiesを使います。asは、実行時または構築手順によって条件を証明済みでもコンパイラが表現できない場所へ限定し、広いスコープへ持ち出しません。
ブランド型は、通常の文字列や数値と混同すると重大な不具合になる識別子や単位に使います。幽霊型は、同じ実行時データに対して処理段階ごとの操作を制限したい場合に使います。どちらも実行時検証の代わりにはならないため、生成経路と外部入力の検証を別に設けます。
実装上の違いと採用条件はブランド型と幽霊型にまとめます。
値がない状態
- プロパティの省略自体に意味がある入力では、オプショナルまたは
undefinedを使う - 値が明示的に存在しないことを保存・通信する契約では、
nullを使う - 未取得、空、削除済みなど意味が異なる状態を、すべて
nullへまとめない - 値の欠如を空文字列で表さない。「意図的に空」と「未入力」を区別できなくなる
PATCHの「変更しない」と「値を消す」のように区別が必要な契約では、undefinedとnullを明確に使い分けます。
Zodでは、省略可能だが処理には必ず値が必要な入力にdefault()を使います。parse後の型からundefinedが除かれ、以降の分岐が不要になります。省略されたこと自体を区別する必要がある場合だけoptional()を使います。