ブランド型
TypeScript で構造的に同じ型を名目的に区別し、型の取り違えを防ぐテクニック
ブランド型とは
ブランド型 (Branded Type) は、TypeScript の構造的型付けでは区別できない型に「ブランド」(タグ) を付けて名目的に区別するテクニックである。UserId と OrderId はどちらも string だが、ブランド型を使えばコンパイル時に取り違えを検出できる。
TypeScript は構造的型付け (Structural Typing) を採用しているため、同じ構造を持つ型は互換性がある。これは柔軟だが、意味的に異なる値の混同を許してしまう。ブランド型はこの問題を型レベルで解決する。
問題: 構造的型付けの落とし穴
// UserId と OrderId はどちらも string → 混同できてしまう
type UserId = string;
type OrderId = string;
function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }
const orderId: OrderId = 'order-123';
getUser(orderId); // コンパイルエラーにならない! 実行時にバグ
この種のバグは単体テストでも見つけにくい。関数の引数の順序を間違えた場合も同様だ。
ブランド型の実装
// ブランドの定義
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type Email = Brand<string, 'Email'>;
// ファクトリ関数で生成
function createUserId(id: string): UserId { return id as UserId; }
function createOrderId(id: string): OrderId { return id as OrderId; }
// 型の取り違えをコンパイル時に検出
function getUser(id: UserId): User { /* ... */ }
const userId = createUserId('usr-123');
const orderId = createOrderId('ord-456');
getUser(userId); // ✅ OK
getUser(orderId); // ❌ コンパイルエラー!
getUser('raw-string'); // ❌ コンパイルエラー!
__brand プロパティは実行時には存在しない。TypeScript のコンパイラだけが認識する「幽霊プロパティ」だ。
バリデーション済みの値を型で表現
ブランド型の強力な応用は、バリデーション済みの値を型で区別することだ。
type ValidatedEmail = Brand<string, 'ValidatedEmail'>;
type PositiveInt = Brand<number, 'PositiveInt'>;
function validateEmail(input: string): ValidatedEmail | null {
return /^[^@]+@[^@]+\.[^@]+$/.test(input) ? (input as ValidatedEmail) : null;
}
function validatePositiveInt(input: number): PositiveInt | null {
return Number.isInteger(input) && input > 0 ? (input as PositiveInt) : null;
}
// sendEmail は ValidatedEmail のみ受け付ける
// → バリデーションを通過していない文字列は渡せない
function sendEmail(to: ValidatedEmail, subject: string): void { /* ... */ }
const email = validateEmail(userInput);
if (email) sendEmail(email, 'Welcome!'); // ✅ バリデーション済み
sendEmail(userInput, 'Welcome!'); // ❌ コンパイルエラー
これにより「バリデーションを忘れた」バグがコンパイル時に検出される。
数値の単位の取り違え防止
type Meters = Brand<number, 'Meters'>;
type Kilometers = Brand<number, 'Kilometers'>;
function metersToKilometers(m: Meters): Kilometers {
return (m / 1000) as Kilometers;
}
const distance = 5000 as Meters;
metersToKilometers(distance); // ✅ OK
metersToKilometers(5 as Kilometers); // ❌ コンパイルエラー
1999 年の Mars Climate Orbiter の墜落事故は、ポンド秒とニュートン秒の単位の取り違えが原因だった。ブランド型があれば、この種のバグはコンパイル時に防げる。
ランタイムコストゼロ
ブランドはコンパイル時のみ存在し、JavaScript にトランスパイルされると消える。as UserId のキャストもランタイムでは何も起きない。パフォーマンスへの影響はゼロだ。
// TypeScript
const id: UserId = 'usr-123' as UserId;
// トランスパイル後の JavaScript (ブランドは消える)
const id = 'usr-123';
Phantom Type との違い
ブランド型と Phantom Type (幽霊型) は似ているが、アプローチが異なる。
| 手法 | 仕組み | 用途 |
|---|---|---|
| ブランド型 | 交差型 (string & { __brand: 'X' }) |
プリミティブ型の区別 |
| Phantom Type | ジェネリクスの型パラメータ | 状態遷移の型安全性 |
ブランド型はプリミティブ型 (string, number) の区別に適し、Phantom Type はオブジェクトの状態遷移 (Draft → Published) の型安全性に適している。
Branded 型の活用場面
| 場面 | 型 | 防ぐミス |
|---|---|---|
| ユーザー ID vs 注文 ID | UserId, OrderId | ID の取り違え |
| 円 vs ドル | JPY, USD | 通貨の混同 |
| 生文字列 vs サニタイズ済み | RawInput, SafeInput | XSS |
詳しくは関連書籍を参照。