【実務・中級編】プリミティブ型をラップする「ブランド型(Branded Types)」によるドメイン駆動設計の実装 – TypeScript コア・型システムの基礎解析バイブル

コードレビューをしていて、最も絶望的な気分になる瞬間を想像してほしい。

function transferFunds(fromAccount: string, toAccount: string, amount: number): void {
// …
}

// そして、呼び出し側がこう書かれていたときだ
transferFunds(amount, toAccount, fromAccount);

TypeScriptを使っていながら、口座ID(`string`)と送金先ID(`string`)が混同され、金額(`number`)の引数に文字列が入り込んでいる。型安全の恩恵を受けているはずのコードベースで、このような「プリミティブの乱用」によるバグが本番環境でしれっと爆発する。

TypeScriptの `string` や `number` は、あまりにも広大すぎる。あらゆる文字列を受け入れてしまうため、ドメイン固有の概念(ユーザーID、注文ID、メールアドレス、金額)を表現するには無力だ。

今回は、このプリミティブの泥沼からコードベースを救い出し、コンパイラの力でビジネスロジックの破綻を物理的に不可能にする「ブランド型(Branded Types)」の極限の知見を伝授する。

—

1. ブランド型とは何か?なぜランタイムコストが「ゼロ」なのか

ブランド型(Nominal Typing imitation)とは、構造的型付け(Structural Typing)を採用しているTypeScriptにおいて、構造が同じプリミティブ型同士を「型レベル」で区別するためにつくられたタグ付け手法だ。

TypeScriptのコンパイラは、本質的に「中身が同じなら同じ型」とみなす(構造的互換性)。

type UserId = string;
type PostId = string;

let userId: UserId = “user_123”;
let postId: PostId = userId; // ❌ エラーにならない!

これでは意味がない。そこで、存在しないプロパティ(ブランド)を交差型(Intersection Types)で付与する。

// ブランディングの基本イディオム
type Brand = K & { readonly __brand: T };

type UserId = Brand;
type PostId = Brand;

ここで重要なのは、この `__brand` プロパティは実行時には一切存在しないということだ。JavaScriptにコンパイルされたとき、これらはただの素の `string` や `number` に戻る。つまり、ランタイムのオーバーヘッドは完全にゼロである。メモリを消費せず、オブジェクトのラップコストもない。純粋にTypeScriptの型チェッカーを欺き、人間のミスを防ぐための「幻のコンパイル時セーフティネット」なのだ。

—

2. プロダクションコードで耐えうる堅牢な実装パターン

「ブランドを付けたはいいが、どうやってインスタンス化するんだ?」という疑問が湧くはずだ。`as UserId` と毎回キャストしていたら、コードのあちこちに型アサーションが散らばり、保守性が最悪になる。

型安全の境界線(Boundary)を厳密に定義した、実務でそのまま使えるプロダクションコードを見てほしい。

/

  • 堅牢なブランド型定義のベースユーティリティ

/
declare const __brand: unique symbol;

type Brand = T & {
readonly [__brand]: TBrand;
};

// — ドメイン固有の型定義 —
export type UserId = Brand;
export type Email = Brand;
export type USD = Brand;

/

  • 型安全なスマートコンストラクタ&バリデーター

/
export const DomainTypes = {
/

  • ユーザーIDの生成と検証

/
userId: (value: string): UserId => {
if (!value.startsWith(“usr_”)) {
throw new Error(`Invalid UserId format: ${value}`);
}
return value as UserId;
},

/

  • メールアドレスの生成と検証(簡易正規表現)

/
email: (value: string): Email => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error(`Invalid Email format: ${value}`);
}
return value as Email;
},

/

  • 金額(USD)の生成と検証

/
usd: (value: number): USD => {
if (value < 0 || !Number.isFinite(value)) { throw new Error(`Invalid USD amount: ${value}`); } return value as USD; } } as const;

この設計の優れている点

1. `unique symbol` の採用: 単なる文字列リテラルではなく一意なシンボルをブランドキーに使うことで、他の型との偶発的な衝突を完全に防ぐ。
2. 境界線(Boundary)の集中管理: プリミティブからブランド型への昇格は、必ず `DomainTypes` のコンストラクタを経由させる。APIレスポンスのパース時や、フォーム入力のバリデーション時に一度ここを通すだけで、以降の関数シグネチャは100%安全になる。

—

3. フロントエンド・API連携における実践的応用

では、これを実際のフロントエンド開発(APIクライアントやコンポーネント設計)でどう活かすか。

// — 1. APIレイヤー —
interface UserResponse {
id: string;
email: string;
balance: number;
}

async function fetchUser(rawId: string): Promise<{ id: UserId; email: Email; balance: USD; }> {
const res = await fetch(`/api/users/${rawId}`);
const data: UserResponse = await res.json();

// 外部からの入力を境界線でブランド型に変換する
return {
id: DomainTypes.userId(data.id),
email: DomainTypes.email(data.email),
balance: DomainTypes.usd(data.balance),
};
}

// — 2. ビジネスロジック・コンポーネント層 —
function sendReceipt(email: Email, amount: USD): void {
console.log(`Sending receipt to ${email} for $${amount}`);
}

async function handleCheckout(rawInputId: string) {
// 境界を通過した安全なデータ
const user = await fetchUser(rawInputId);

// ❌ 完全にコンパイルエラーになる例
// sendReceipt(user.id, user.balance);
// 引数の順序ミスや、UserIdをEmailの代わりに渡すミスを型が即座に弾く。

// ✅ 正しい呼び出し
sendReceipt(user.email, user.balance);
}

IDE(VSCodeなど)の補完を想像してほしい。`sendReceipt` の引数にカーソルを合わせたとき、そこには単なる `string` ではなく `Email` や `USD` という「文脈」が表示される。ドキュメントを読まなくても、型定義そのものが仕様書として機能するようになる。

—

4. チーフアーキテクトからの警告:パフォーマンスと運用の罠

ブランド型は強力だが、銀の弾丸ではない。誤った使い方をすると、かえってコードベースを蝕むことになる。以下の2点に注意せよ。

① 過剰なブランディング(Over-Branding)の禁止

すべての `string` にブランドを付ける必要はない。単なる汎用的なコンポーネントのタイトルや、ログメッセージ、URLの一部など、ドメインの整合性に影響を与えないものにまでブランド型を強制すると、単なるボイラープレート(冗長なコード)の増加を招き、開発体験が著しく悪化する。
「ドメインの境界で値が混同されると致命的なバグにつながる重要データ」にのみ絞って適用せよ。

② サードパーティライブラリとの境界

データベースのORM(PrismaやDrizzleなど)や、Zodなどのバリデーションライブラリとの統合においては、型アサーションやZodの `.brand<"UserId">()` 機能を活用し、シームレスにブランド型へ移行するパイプラインを構築すること。例えばZodであれば以下のようにスマートに定義できる。

import { z } from “zod”;

export const UserIdSchema = z.string().uuid().brand(“UserId”);
export type UserId = z.infer;

—

総括:型安全とは「人間の認知負荷を減らす技術」である

優れた型設計とは、コードを書いている最中に「あ、これ間違ってるかも」とプログラマが不安になる余地を、コンパイラに肩代わりさせることだ。

プリミティブ型にドメインの文脈(ブランド)を纏わせることで、「うっかりミス」はコンパイルエラーという即座のフィードバックによって開発者の手元で潰され、本番環境には絶対に到達しなくなる。

今日から君のプロジェクトでも、ただの `string` や `number` の裸の変数を使うのをやめよう。コードの意図を型に語らせ、圧倒的な堅牢性を手に入れてほしい。

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