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

こんにちは!TypeScriptの型システムの世界へようこそ。
日々、フロントエンドからバックエンドまでコードを書いていると、「あ、これ、普通の文字列(`string`)なんだけど、データベースの『ユーザーID』であって、画面に入力された『普通のメモ』じゃないんだよな…」という場面に直面しませんか?

JavaScriptや、素のTypeScriptでは、どちらも単なる `string` として扱われてしまいますよね。
そのため、うっかりユーザーIDを渡すべき場所に、メモの文字列をそのまま渡しバグを生んでしまう……なんて事故も起こりがちです。

今回は、そんなプリミティブ地獄から私たちを救い出し、型レベルでドメイン(業務領域)の安全性を完璧に守り抜く「ブランド型(Branded Types)」について、優しく、そして本質的に解説していきますね。

ここをクリアすれば、TypeScriptの型推論と構造的型付けの裏側まで見えてきます。一緒にバッチリマスターしていきましょう!

—

1. なぜ「ただの string / number」では危険なのか?

まずは、私たちが普段やりがちなコードを見てみましょう。

type UserId = string;
type ProductId = string;

function processOrder(userId: UserId, productId: ProductId) {
// 処理…
}

const rawUserId: UserId = “user_123”;
const rawProductId: ProductId = “prod_999”;

// 【危険】引数の順番を間違えても、TypeScriptは何も文句を言わない!
processOrder(rawProductId, rawUserId);

おや? `processOrder` の第一引数には `UserId` を渡すべきなのに、うっかり `ProductId` を渡してしまっています。
しかし、コンパイルエラーは起きません。なぜなら、TypeScriptは「構造的型付け(Structural Subtyping)」を採用しているからです。

TypeScriptにとって、どちらも「中身が `string` のデータ」であれば、名前が違っていても同じものとみなしてしまうのです。これが、プリミティブ型だけを使った設計の限界であり、恐ろしいところですね。

—

2. ブランド型(Branded Types)の基本構造:型に「烙印(ブランド)」を押す

ここで登場するのが「ブランド型」です。
考え方はとてもシンプルで、実行時の値はただの `string` や `number` のまま、「型システムの上だけで、見えないハンコ(ブランド)を押す」というテクニックです。

実際のコードを見てみましょう。

// ブランド型を定義するためのヘルパー(イディオム)
type Brand = T & { readonly __brand: TBrand };

// それぞれ独自の「烙印」を持つ型を定義
type UserId = Brand;
type ProductId = Brand;

ここで何が起きているか、頭の中で型パズルを解いてみましょう。
`Brand` は、「中身は `string`」でありつつ、`{ readonly __brand: “UserId” }` という絶対に実行時には存在しない(けれど型推論上は存在する)プロパティを交差(Intersection)させたものです。

この「見えないハンコ」があるおかげで、TypeScriptは「あ、これはただの文字列じゃなくて、`UserId` という特権階級の文字列だな」と区別できるようになります。

—

3. 実践:安全なファクトリー関数とドメイン駆動設計

「でも、ただの `string` にそんなハンコをどうやって押すの? 手動でキャストするの?」という疑問が湧きますよね。
もちろん、毎回 `as UserId` と書くのはバグの元です。ここでスマートなファクトリー関数(コンストラクタ関数)の出番です。

// 安全に UserId を生成するファクトリー関数
function createUserId(id: string): UserId {
// ここでバリデーション(UUIDの形式チェックなど)を行えるのが最高に強い!
if (!id.startsWith(“usr_”)) {
throw new Error(“無効なユーザーIDの形式です”);
}
// 実行時はただの string だが、型システムを騙して(アサーションして)ブランドを付与
return id as UserId;
}

function createProductId(id: string): ProductId {
if (!id.startsWith(“prd_”)) {
throw new Error(“無効な商品IDの形式です”);
}
return id as ProductId;
}

実際の使用イメージ

function processOrder(userId: UserId, productId: ProductId) {
console.log(`Processing order for user: ${userId}, product: ${productId}`);
}

// 1. 正しい生成と呼び出し
const userId = createUserId(“usr_12345”);
const productId = createProductId(“prd_98765”);

processOrder(userId, productId); // 完璧にコンパイルが通る!

// 2. 存在しない、あるいは不正な文字列をそのまま渡そうとすると…?
// processOrder(“usr_12345”, “prd_98765”);
// ❌ エラー! “usr_12345” は stringであって、UserId型(__brandを持つ)ではないため怒られる!

// 3. 引数の順序ミスも一発で検知!
// processOrder(productId, userId);
// ❌ エラー! ProductId を UserId の場所に渡すことはできません!

どうですか? 実行時のオーバーヘッドはゼロ(ただの文字列のまま)なのに、開発中には絶対にミスができない、要塞のような型安全性が手に入りましたよね。これがドメイン駆動設計(DDD)における「値オブジェクト(Value Object)」のTypeScript的な極みです。

—

4. 陥りやすい文法エラーと注意点

ブランド型を使う上で、初学者がよくハマるポイントをいくつか押さえておきましょう。

① `readonly __brand` を付ける理由

`{ __brand: TBrand }` と記述した際、`readonly` を付け忘れると、予期せぬタイミングでプロパティが書き換わったり、型互換性の問題(変性に関するトラブル)に巻き込まれやすくなります。基本的には `readonly` を付けて「不変のハンコ」にしておきましょう。

② 演算時の注意(number型の場合など)

金額(`Yen`)などをブランド型にする場合、計算をすると型が崩れることがあります。

type Yen = Brand;

const price1 = 100 as Yen;
const price2 = 200 as Yen;

// ❌ エラーになることがある(number同士の足し算の結果は、Yenブランドが剥がれた通常のnumberになるため)
// const total: Yen = price1 + price2;

// 正しくは、計算後に再度ブランドを付与するか、専用の演算関数を用意する
const total = (price1 + price2) as Yen;

このように、プリミティブな演算を行った結果はベースの型(`number`や`string`)に戻ってしまうため、足し算や文字列結合を行うヘルパー関数をドメイン層に用意してあげると、さらに美しいコードになります。

—

まとめ

いかがだったでしょうか?

  • 構造的型付けの限界: 単なる `string` や `number` では、意味の異なる値同士の混同を防げない。
  • ブランド型(Branded Types): 交差型を使って型レベルだけで識別子を付与し、構造的型付けの隙を埋める。
  • ファクトリー関数の併用: 生成時にバリデーションとブランド付与を同時に行うことで、ドメインの整合性を担保する。

ここをクリアできれば、あなたの書くTypeScriptコードは、単なる「JavaScriptに毛が生えたもの」から、「コンパイラを味方につけた堅牢なシステム」へと劇的に進化します。

現場のコードでもぜひ取り入れて、型安全の心地よさを実感してみてくださいね。それでは、次のステップでも一緒に楽しく学んでいきましょう!

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