principle-type-system-discipline について
言語を問わず、型システムで無効な状態を表現不能にし、未処理のケースをコンパイル時に見つける開発スキルです。型付き言語の開発者を対象に、堅牢なモデル、境界での解析、網羅的な処理を重視します。
主な機能
- 01不正な状態を表現不能にする
- 02意味の異なる基本型を区別する
- 03境界で型のないデータを解析
- 04型チェックを迂回するキャストを回避
- 05すべてのバリアントを網羅的に処理
- 06信頼できるスキーマから型を導出
活用シーン
- 01相互排他的なアプリ状態のモデル化
- 02意味の異なる ID の混同防止
- 03API や設定データの検証
- 04追加されたバリアントの処理漏れ確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ hachej/ boring-stack --skill principle-type-system-disciplineインストール後、Claude Code で/principle-type-system-disciplineを入力して読み込まれたか確認します。
同梱ファイルLICENSE
詳細
型システムの規律
型チェッカーは証明支援系です。これを使って、あり得ない状態、プリミティブ型の不一致、未処理のバリアントをコンパイル時に排除してください。型によって無視できてしまうケースは、コンパイラーが防げたはずのランタイム障害につながります。ハンドラーを増やすより、エラーや特殊ケース自体を存在しないようにすることを優先してください。表現不可能な状態、全域関数、インターフェースの再設計(以下のパターン)がそのための手段です。
型付き言語全般に適用できます。typescript-best-practices などのスキルは、具体的な構文を使ってこの原則を説明しています。
パターン:
- 不正な状態を表現不可能にする。 バリアントを代数的データ型でモデル化します。TypeScript では判別共用体、Rust/Swift/Kotlin ではペイロードを持つ列挙型、Scala ではシールドクラス、Haskell/OCaml では ADT を使います。矛盾する組み合わせでもコンパイルできてしまう、任意のフィールドを集めた状態表現は避けてください。見落としがちなアンチパターンとして、
{ completed: boolean; completedAt?: Date }はcompleted: true; completedAt: undefinedを許してしまいますが、これは意味のない状態です。completedAt !== nullのような単一の情報源から真偽値を導出するか、{ kind: 'open' } | { kind: 'done'; at: Date }のようにバリアントを明示的にモデル化してください。バグに直面して「待って、この組み合わせは本当に起こり得るのか?」と疑問に思うなら、型が緩すぎます。 - 型は制約ではなく構築物です。 緩い型にチェックを加えて望む値を切り出すのではなく、望む値から型を組み立ててください。絞り込み型が必要に思える不変条件も、たいていは構築方法を変えるだけで表現できます。空でないリストは、長さチェック付きのリストではなく、先頭要素と残りの要素で表します。有効な時間範囲は、順序を保つ必要がある2つのタイムスタンプではなく、開始時刻と期間で表します。特定の表現だけを優先する必要はありません。ペアのリストは、そう解釈すれば要素数が偶数のリストです。したがって、不正な値を構築できない形を選び、その上に呼び出し元が必要とするインターフェースを公開してください。
- 意味を持つプリミティブ型にはブランドを付ける。
UserIdとOrderIdは内部的にはどちらも文字列ですが、相互に置き換えられるべきではありません。Rust の newtype、Swift の不透明型、Kotlin の値クラス、Haskell のファントム型、TypeScript のブランド付き交差型を使います。作成時に一度だけ検証し、以降は型を信頼してください。 - 外部データはパースするまで型なしとして扱う。 RPC ペイロード、JSON、IPC メッセージ、CLI 引数、設定ファイル、環境変数、データベースの行が対象です。すべての境界にパース関数を設け、構造化されていない入力を型付きモデルに変換してください。検証をどこに配置するかについては、boundary-discipline 原則スキルを参照してください。
- 型システムに嘘をつかない。 コンパイラーを迂回するキャスト、安全でない型強制、アサーション関数は、潜在的なランタイムクラッシュの原因です。コンパイラーが事実を証明できないなら、その事実を証明する(検証する、型を絞り込む、モデルを精緻化する)か、キャストが危険であることを受け入れてください。
- 網羅的なパターンマッチングはコンパイラーの役目です。 代数的データ型をマッチングするとき、新しいバリアントが追加されたのに処理されていなければ、コンパイラーがコンパイルに失敗する必要があります。言語が提供する慣用的な方法を使ってください。TypeScript では
never型のバインディング、Rust では注釈なしのmatch、Haskell では-Wincomplete-patterns、Kotlin ではシールドクラスのマッチングにおける網羅性チェックです。 - 信頼できるスキーマから型を導出する。 Protocol Buffers、OpenAPI 仕様、GraphQL スキーマ、データベースのマイグレーション、デザインシステムのトークンファイルが形状を定義しているなら、それに準拠する別の型を手作業で作るのではなく、そこから型を導出してください。encode-lessons-in-structure 原則スキルを参照してください。
- 部分性が現れる箇所でのみ型を強化する。 ランタイムアサーション、null チェック、「これは決して起こらないはず」という例外のスローは、型が弱すぎる箇所を示しています。そのチェックを型に反映してください。そして、そこで止めてください。型システムの役割は、各使用箇所で処理すべきケースを追跡することであり、データを可能な限り精密に記述することではありません。全域関数を優先してください。空のリストの
sumは0なので、通常のリストを受け取ります。空のリストのheadには答えがないので、空でないリストを要求します。
テスト:
- 「このフィールドの組み合わせが有効になる条件を説明するコメントを書けますか?」書ける場合、その型は緩すぎます。直和型に分けてください。
- 「関数の引数のうち、プリミティブ型は同じでも意味が異なるものはありますか?」ブランド型にしてください。
- 「この
any、このas、このassertNotNullはどこから来たのでしょうか?」境界までたどり、そこで検証してください。 - 「来月、新しいバリアントが追加されたとき、コンパイラーは次のエージェントにケースを追加する場所を知らせてくれますか?」そうでなければ、マッチが網羅的ではありません。
- 「この型は、別のファイルが所有する形状を重複して定義していませんか?」代わりに導出してください。
- 「操作を全域的に保つために、この型をより厳密にしていますか?それとも、単に精度を高めようとしているだけですか?」そうしなければパニックが起きる場面がないなら、通常の型のままにしてください。
コメント
Markdown 対応