ブランド型

TypeScript で構造的に同じ型を名目的に区別し、型の取り違えを防ぐテクニック

TypeScript型安全

ブランド型とは

ブランド型 (Branded Type) は、TypeScript の構造的型付けでは区別できない型に「ブランド」(タグ) を付けて名目的に区別するテクニックである。UserIdOrderId はどちらも 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

詳しくは関連書籍を参照。

関連用語