【入門編】Type Aliasによる「ブランド型(Branded Types)」を用いたドメイン駆動設計 – TypeScript コア・型システムの基礎解析バイブル

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` を使ってみてください。コンパイラがあなたの代わりに、最強のコードレビューをしてくれるはずですよ。

タイトルとURLをコピーしました