幽霊型
ファントムタイプ(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の型システムはこのプロパティを認識し、異なる型パラメータを持つインスタンスを区別します。これが「幽霊型」と呼ばれる理由です。
よくある問題
ファントムプロパティの!の意味
!は確実代入アサーション演算子で、プロパティが初期化されていなくてもエラーにならないようにします。ファントムタイプは実行時に使用されないため、初期化は不要です。
パフォーマンスへの影響
ファントムタイプは型レベルのみの機能で、実行時にはプロパティも値も存在しません。
既存コードへの適用
既存のnumberやstringを段階的にファントムタイプに移行できます。型エイリアスから始めて、必要に応じてクラスベースの実装に移行します。
ジェネリック制約の追加
ファントムタイプに制約を追加して、特定の型のみを受け入れるようにできます。
// 通貨型の制約
interface CurrencyType {
currency: string;
symbol: string;
}
class Money<T extends CurrencyType> {
private readonly _phantom!: T;
// ...
}