想定外の状態は型で表現できなくする

Updated 2025-12-30 programming · TypeScript · Kotlin · agentic coding

バグが発生する原因のひとつに「想定外の状態」がある。

  • a. 存在しない値の参照 (e.g. NullPointerException)
  • b. 想定外の状態遷移
  • c. 処理し忘れた分岐

これらを型で表現することでエージェントがコンパイラで検証できれば、安全なコードを書く確率が高くなる。具体的には

  • a. non-null にする
  • b. sealed / discriminated union で想定している状態を明示する
  • c. Result/Either とパターンマッチングで網羅性チェックする(期待されるエラーは値で返して網羅的に扱う)

を意識して型を定義する。最近のエージェントは賢いので、コンパイルを通すだけのコードやなくて意図を汲み取ったコードを書いてくれる。

ドメインモデルを設計する

Twitter のようなアプリケーションをつくるとして、バックエンドを TypeScript で書いたらこんなイメージ。

// Branded Types
type UserId = string & { readonly brand: unique symbol };
type TweetId = string & { readonly brand: unique symbol };

// タイムラインのアイテム種別をdiscriminated unionで表現してnullを防ぐ
type TimelineItem =
  | { type: "tweet"; tweet: TweetContent }
  | { type: "retweet"; retweetedBy: User; tweet: TweetContent }
  | { type: "ad"; ad: AdContent };

// 次のカーソルがnullableなんはしゃーない
type Timeline = {
  items: TimelineItem[];
  nextCursor: string | null;
};

同じ考え方をアプリ側にも持ち込む。Kotlin なら sealed class で UI の状態を定義する。

// ホーム画面の状態
sealed class HomeUiState {
    data object Loading : HomeUiState()
    data class Success(val timeline: List<TimelineItem>) : HomeUiState()
    data class Error(val error: HomeError) : HomeUiState()
}

// タイムラインのアイテム種別
// nullableなプロパティがなくなる
sealed class TimelineItem {
    data class Tweet(val tweet: TweetContent) : TimelineItem()
    data class Retweet(val retweetedBy: User, val tweet: TweetContent) : TimelineItem()
    data class Ad(val ad: AdContent) : TimelineItem()
}

Compose の UiState を Sealed Interface にする話(表示に使ってよいのは UiState だけ)は、これを UI 層でやっているのと同じことになる。エージェントを使うようになる前から効く設計だったと言える。

なぜこれをエージェントに対してやるのかは 型はレビューで伝えていた知識を運ぶ に書いた。

このノートにリンクしているノート

  • 期待されるエラーは値で返して網羅的に扱う
  • 型はレビューで伝えていた知識を運ぶ
  • 表示に使ってよいのは UiState だけ
Michiaki Mizoguchi

Michiaki Mizoguchi

Software Developer / Engineering Manager

Home Notes Resume GitHub