Interactive

幽霊型

ファントムタイプ(Phantom Type、幽霊型)は実行時に存在しない型パラメータを使って、コンパイル時の型安全性を提供するパターンです。ClaudeCodeはこのパターンを使って、異なる単位や状態を型レベルで区別できます。

弊社の推奨ルール

  • 単位の混同や不正な操作を防ぐ
  • _phantom!プロパティで型の区別を強制

ClaudeCodeでの利用

型安全な通貨管理

異なる通貨の混同を防いで計算ミスを回避したい場合

「通貨の型安全性を保証するValueクラスを幽霊型で作成」

状態遷移の型チェック

ワークフローの状態遷移を型レベルで強制したい場合

「ドキュメントのワークフローを幽霊型のステートパターンで実装」

バリデーション状態の区別

データのバリデーション前後を型レベルで明確に区別する場合

「ユーザーのValidated/Unvalidated状態を幽霊型で管理」

リソース管理

ファイルハンドルなどのリソース状態を型安全に管理する場合

「ファイルのOpen/Closed状態を幽霊型で管理してリソースリークを防ぐ」

通貨の型安全な管理

異なる通貨を誤って混同することを防ぐ、より実践的な例を見てみます。

class Money<Currency> {
  // 幽霊プロパティで通貨を区別
  private readonly _phantom!: Currency;

  constructor(private readonly amount: number) {
    Object.freeze(this);
  }

  // 金額の取得
  getAmount(): number {
    return this.amount;
  }

  // 同じ通貨同士の加算
  add(other: Money<Currency>): Money<Currency> {
    return new Money<Currency>(this.amount + other.amount);
  }

  // 同じ通貨同士の減算
  subtract(other: Money<Currency>): Money<Currency> {
    return new Money<Currency>(this.amount - other.amount);
  }

  // 型安全な通貨変換
  convert<TargetCurrency>(
    rate: number,
    _marker: TargetCurrency
  ): Money<TargetCurrency> {
    return new Money<TargetCurrency>(this.amount * rate);
  }

  // フォーマット済み文字列の取得
  format(symbol: string): string {
    return `${symbol}${this.amount.toFixed(2)}`;
  }
}

// 通貨の型定義
type USD = { currency: "USD" };
type JPY = { currency: "JPY" };
type EUR = { currency: "EUR" };

// ファクトリー関数
function usd(amount: number): Money<USD> {
  return new Money<USD>(amount);
}

function jpy(amount: number): Money<JPY> {
  return new Money<JPY>(amount);
}

// 使用例
const price = usd(99.99);
const tax = usd(10.00);
const total = price.add(tax);

const yenPrice = jpy(11000);

// ❌ エラー: 異なる通貨の演算は不可
// const invalid = price.add(yenPrice); // コンパイルエラー

// ✅ 明示的な変換は可能
const yenTotal = total.convert(110, {} as JPY);

_phantomプロパティの役割

ファントムタイプの核心は_phantomプロパティにあります。このプロパティは実行時には使用されませんが、TypeScriptの型システムに型の違いを認識させる役割を果たします。

なぜ_phantomプロパティが必要か

TypeScriptは構造的型付け(Structural Typing)を採用しているため、同じ構造を持つ型は互換性があるとみなされます。_phantomプロパティがないと、型の区別ができません。

// 構造的型付けの問題
class SimpleValue<T> {
  constructor(private value: number) {}
  getValue(): number { return this.value; }
}

type USD = { currency: "USD" };
type JPY = { currency: "JPY" };

const dollars = new SimpleValue<USD>(100);
const yen = new SimpleValue<JPY>(10000);

// ⚠️ 問題:構造が同じなので代入できてしまう!
const wrong: SimpleValue<USD> = yen; // エラーにならない

// これは以下と同等の構造だから
// class SimpleValue {
//   private value: number;
//   getValue(): number;
// }

_phantomプロパティで型を区別

_phantomプロパティを追加することで、TypeScriptの型システムに構造的な違いを作り出し、異なる型パラメータを持つクラスを区別できるようにします。

class Value<T> {
  // このプロパティが型Tを「運ぶ」役割を果たす
  private readonly _phantom!: T;
  
  constructor(private value: number) {}
  getValue(): number { return this.value; }
}

const dollars = new Value<USD>(100);
const yen = new Value<JPY>(10000);

// ✅ 正しい:型が異なるのでエラーになる
const correct: Value<USD> = yen; // コンパイルエラー!

// なぜエラーになるか:
// Value<USD>の構造: { _phantom: USD, value: number, getValue(): number }
// Value<JPY>の構造: { _phantom: JPY, value: number, getValue(): number }
// _phantomの型が異なるので、構造的に互換性がない

!(確実代入アサーション)の役割

!はプロパティが初期化されなくてもエラーにしないことをTypeScriptに伝える演算子です。幽霊型では実行時に使わないプロパティであるため、初期化をスキップするために使用します。

class Value<T> {
  // !がない場合:エラー「プロパティ '_phantom' に初期化子がなく、
  // コンストラクターで明確に割り当てられていません」
  private readonly _phantom: T; // エラー!
  
  // !をつけることで「このプロパティは使わないが型情報のためだけに存在する」
  // ことをTypeScriptに伝える
  private readonly _phantom!: T; // OK
  
  constructor(private value: number) {
    // _phantomは実際には初期化しない(実行時には存在しない)
  }
}

_phantomプロパティは実際には初期化されず、実行時には存在しません。しかし、TypeScriptの型システムはこのプロパティを認識し、異なる型パラメータを持つインスタンスを区別します。これが「幽霊型」と呼ばれる理由です。

よくある問題

ファントムプロパティの!の意味

!は確実代入アサーション演算子で、プロパティが初期化されていなくてもエラーにならないようにします。ファントムタイプは実行時に使用されないため、初期化は不要です。

パフォーマンスへの影響

ファントムタイプは型レベルのみの機能で、実行時にはプロパティも値も存在しません。

既存コードへの適用

既存のnumberstringを段階的にファントムタイプに移行できます。型エイリアスから始めて、必要に応じてクラスベースの実装に移行します。

ジェネリック制約の追加

ファントムタイプに制約を追加して、特定の型のみを受け入れるようにできます。

// 通貨型の制約
interface CurrencyType {
  currency: string;
  symbol: string;
}

class Money<T extends CurrencyType> {
  private readonly _phantom!: T;
  // ...
}