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

TypeScriptの「型」でドメインを封じ込める:Branded Typesによる堅牢な設計術

Web開発の現場で、私たちは日々「プリミティブな型」の暴力に晒されています。

`string`、`number`、`boolean`。これらは便利ですが、ドメインの文脈を無視します。`UserId`も`ProductId`も`Email`も、すべて単なる`string`として扱われていないでしょうか?その結果、IDを入れ替えて関数に渡してもコンパイラは沈黙し、本番環境で予期せぬデータ破壊が起きる。

これは「型システム」を使っているようで、実際には「型」の力をドメイン層で放棄しているに等しい。

今回は、TypeScriptの型システムをハックし、コンパイル時にドメインの整合性を強制する「ブランド型(Branded Types)」の極致を伝授します。

—

1. なぜ「ただのstring」では不十分なのか

以下のような関数を想像してください。

function deleteUser(userId: string) { / … / }
function deleteProduct(productId: string) { / … / }

const productId = “PROD_123”;
deleteUser(productId); // コンパイルエラーにならない!

TypeScriptは構造的部分型(Structural Subtyping)を採用しています。`string`はどこまで行っても`string`であり、意味論的な区別をコンパイラは理解しません。これを放置するのは、「型安全という防具を捨てて戦場に出る」のと同じです。

2. Branded Types:型に「意味」という烙印を押す

解決策は、型に「ブランド」を付与することです。これにより、コンパイラは「中身は文字列だが、これは特定の文脈の文字列である」と認識するようになります。

実装パターン

/

  • ブランド型を生成するためのユーティリティ
  • __brandという「存在しないプロパティ」を付与することで、
  • 構造的部分型の判定を回避する

/
type Brand = K & { __brand: T };

type UserId = Brand;
type Email = Brand;

// コンストラクタ関数(ランタイムでの検証を忘れずに)
const toUserId = (id: string): UserId => id as UserId;
const toEmail = (id: string): Email => {
if (!id.includes(“@”)) throw new Error(“Invalid Email”);
return id as Email;
};

function sendNotification(email: Email, userId: UserId) {
console.log(`Sending to ${email} for user ${userId}`);
}

const myId = toUserId(“user_001”);
const myEmail = toEmail(“test@example.com”);

sendNotification(myEmail, myId); // OK
// sendNotification(myId, myEmail); // コンパイルエラー!

なぜこれで上手くいくのか

TypeScriptのコンパイラは、`{ __brand: “UserId” }` を持つ型と、そうでない型を比較する際、このプロパティの有無を厳密にチェックします。ランタイム時にはこのプロパティは存在しない(消去される)ため、パフォーマンスへの影響はゼロです。これがTypeScriptの真骨頂です。

—

3. 実践:ドメイン層での活用と保守性

大規模開発では、単なる型定義だけでなく、ドメイン層での境界線(Boundary)を意識することが重要です。

現場で使うための「型安全なID生成器」

// ID生成の責務をここに集中させる
export const createId = (prefix: string): Brand => {
return `${prefix}_${Math.random().toString(36).slice(2)}` as Brand;
};

// 使用例
type OrderId = Brand;
const orderId = createId<"OrderId">(“ORD”); // 型安全に生成

パフォーマンスと注意点

  • ランタイムコスト: ゼロです。型情報はコンパイル時にすべて消去されます。
  • シリアライズ: APIのJSONレスポンスを扱う際は注意が必要です。外部から受け取ったデータは`any`や`unknown`として受け取り、必ずドメイン層の境界で`toUserId()`のような関数を通してください。ここをサボると、ブランド型はただの「お飾り」になります。

—

4. チーフアーキテクトからの助言

Branded Typesを導入すると、最初は「キャスト(`as`)が面倒だ」と感じるかもしれません。しかし、その「書くのが面倒な箇所」こそが、本来バグが紛れ込んでいた場所なのです。

1. 境界で守る: 外部APIからの入力、DBからの取得。この「境界」を越える場所でのみ、`as`を使った変換を許可する。
2. ロジックには持ち込まない: ビジネスロジック内部では、常にブランド型のみを使用する。
3. チームへの教育: 「なぜここで `as` を使うのか?」を問われたとき、「これはドメインへの入り口だからだ」と即答できるチームを作りましょう。

型は「制約」であると同時に、ドメインを記述するための「言語」です。TypeScriptの型システムを使いこなし、コンパイラを最強のレビューアに変えてください。

あなたの書くコードが、コンパイルを通るだけで正しさを保証する――。それこそが、エンジニアが追求すべき「美学」です。

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