TypeScriptの「型安全」を次の次元へ:ブランド型(Branded Types)でドメインを掌握する
こんにちは。日々TypeScriptの型システムと対話していると、「`string`や`number`だけでコードを書いていて本当に大丈夫?」と不安になる瞬間はありませんか?
例えば、`userId`も`productId`も、どちらも`string`型です。これらを関数の引数に渡すとき、TypeScriptは「どちらも文字列だからOK!」と判定してしまいます。結果、IDの混入によるバグが発生する……これは現場でよくある悲劇です。
今回は、TypeScriptの型システムをハックして、「プリミティブな型に魂(アイデンティティ)を宿す」ためのテクニック、ブランド型(Branded Types)を徹底解説します。
—
1. なぜ「ただのstring」では不十分なのか?
まずは、私たちがよくやる「素朴な実装」を見てみましょう。
type UserId = string;
type ProductId = string;
function deleteProduct(id: ProductId) {
// 本来はProductを消すべきなのに、UserIdを渡してもコンパイルが通ってしまう
console.log(`Deleting product: ${id}`);
}
const myUserId: UserId = “user_123”;
// 致命的なバグ:ユーザーIDを渡してもTypeScriptは文句を言わない
deleteProduct(myUserId);
TypeScriptの型システムは「構造的部分型(Structural Subtyping)」を採用しています。「中身が同じなら同じもの」とみなす、非常に柔軟で強力な仕組みです。しかし、ドメイン駆動設計(DDD)のような厳密な境界線を引く世界では、この柔軟さが仇となります。
—
2. ブランド型(Branded Types)という魔法
ここで登場するのが「ブランド型」です。これは、型に「タグ(刻印)」を押すことで、コンパイル時のみ、同じプリミティブ型同士を区別させる手法です。
実装の基本形
// 1. ブランド化するための型定義
interface Brand {
readonly _brand: B; // これが「刻印」の正体です
}
// 2. プリミティブとブランドを交差(Intersection)させる
type UserId = string & Brand<"UserId">;
type ProductId = string & Brand<"ProductId">;
// 3. 値をブランド型に変換するヘルパー関数(キャスト)
function createUserId(id: string): UserId {
return id as UserId;
}
function deleteProduct(id: ProductId) {
console.log(`Deleting product: ${id}`);
}
const myUserId = createUserId(“user_123”);
// deleteProduct(myUserId);
// !!ここでコンパイルエラー!!
// Argument of type ‘UserId’ is not assignable to parameter of type ‘ProductId’.
なぜこれで区別できるのか?
TypeScriptのコンパイラは、型を評価する際、`string`という中身だけでなく、`_brand`という架空のプロパティ(実際には存在しなくても良い)の有無をチェックします。「`UserId`という刻印を持つ文字列」と「`ProductId`という刻印を持つ文字列」は、コンパイラにとって「構造が異なる別の型」になるからです。
—
3. 現場で「陥りやすい罠」と対策
ブランド型を使う上で、初心者が必ず直面する壁がいくつかあります。
罠1:リテラルや変数を直接代入しようとする
const badId: UserId = “user_123”;
// エラーは出ませんが、これは型安全ではありません。
// 常に「コンストラクタ関数(createUserIdなど)」を通す運用にしましょう。
教訓: ブランド型は「型を隠蔽(カプセル化)」して初めて価値が出ます。値を作る入り口を関数に限定してください。
罠2:実行時のオーバーヘッドを気にする
「`_brand`プロパティをオブジェクトに追加するの?」と心配になるかもしれませんが、安心してください。TypeScriptの型システムはコンパイル時に消滅します。実行時にはただの`string`としてメモリ上に存在するため、パフォーマンスへの影響は皆無です。
—
4. ドメイン駆動設計(DDD)への適用
ドメイン層において、`Email`、`Password`、`Username`などをブランド型として定義すると、関数の引数の順序を間違えるといった「ケアレスミス」を、コンパイラが完全にシャットアウトしてくれます。
type Email = string & Brand<"Email">;
function sendWelcomeEmail(email: Email) {
// ここに来る時点で、emailは「Emailとしての形式を満たした文字列」であることが保証される
}
「型安全である」ということは、「実行時に検証ロジックを何度も書かなくて済む」という開発者体験の向上に直結します。
—
最後に:型を「制約」ではなく「武器」にする
ブランド型を使いこなすと、TypeScriptのコードは「データが何者であるか」をより雄弁に語るようになります。
最初は少し面倒に感じるかもしれません。しかし、大規模なアプリケーションになればなるほど、この小さな「刻印」が、あなたのコードをバグという名の嵐から守る盾になってくれます。
ここをクリアすれば、あなたはもうTypeScriptの初学者ではありません。型システムを意のままに操り、堅牢なアーキテクチャを構築するエンジニアへの第一歩を踏み出したのです。
ぜひ、次のプロジェクトで `_brand` を使ってみてください。コンパイラがあなたの代わりに、最強のコードレビューをしてくれるはずですよ。