こんにちは。TypeScriptという言語の深淵へようこそ。
多くの開発者が、TypeScriptを単なる「JavaScriptに型が付いたもの」と誤解しています。しかし、真の使い手にとってTypeScriptは、「ドメインの概念をコードに書き込み、コンパイラを最強のレビューアーに変えるための言語」です。
今日は、DDD(ドメイン駆動設計)の入り口であり、TypeScriptを使いこなすための最大の武器、「ブランド型(Branded Types)」についてお話ししましょう。
—
なぜ「ただのstring」ではいけないのか?
DDDにおいて、プリミティブな型(stringやnumber)をそのまま使うことは「プリミティブ執着(Primitive Obsession)」というアンチパターンとみなされます。
例えば、ユーザーIDとメールアドレスを考えてみましょう。
type UserId = string;
type Email = string;
function sendEmail(id: UserId, email: Email) {
// ここで誤って sendEmail(email, id) と渡してしまっても、
// TypeScriptは何も文句を言いません。
// なぜなら、どちらもただの「string」だからです。
}
コンパイラは「文字列であること」しか確認しません。しかし、ビジネスドメインの世界では「ユーザーID」と「メールアドレス」は全く別の概念ですよね。ここを型レベルで分離する技術が、ブランド型です。
—
ブランド型(Branded Types)の魔法
ブランド型とは、TypeScriptの構造的部分型(Structural Typing)の性質を逆手に取り、型に「刻印(Brand)」を押す手法です。
実装の基本形
// インターセクション型(&)を使って、型に「看板」をぶら下げます
type UserId = string & { readonly __brand: ‘UserId’ };
type Email = string & { readonly __brand: ‘Email’ };
// 値を生成するヘルパー関数
function createUserId(id: string): UserId {
return id as UserId; // コンパイラを説得して型をキャスト
}
このコードのポイントは、`__brand` というプロパティです。実行時にはこのプロパティは存在しません(型システム上の幻影です)。しかし、コンパイル時にはこれが「壁」として機能します。
なぜこれで安全になるのか?
function sendEmail(id: UserId, email: Email) {
console.log(`Sending to ${id} at ${email}`);
}
const myId = createUserId(“user-123”);
const myEmail = “test@example.com” as Email;
// OK: 型が一致している
sendEmail(myId, myEmail);
// エラー!: 引数が逆だと、ブランドが一致しないためコンパイルエラーになる
// Argument of type ‘Email’ is not assignable to parameter of type ‘UserId’.
sendEmail(myEmail, myId);
どうでしょう? コンパイラが「それはEmailであってUserIdではない」と怒ってくれるようになりました。これで、関数の引数の順番間違いという「よくある事故」を、コードを書いている瞬間に封じ込めることができます。
—
陥りやすい罠:なぜ「readonly」が必要なのか?
初心者がよく忘れるのが `readonly` の付与です。
もし `readonly` を付けないと、TypeScriptは「構造的に互換性があるなら同じ型だ」とみなしてしまい、意図しない場所で型が混ざってしまうリスクがあります。また、`__brand` 自体は値を持たない(空オブジェクトのようなもの)ため、誤って値を書き換えてしまう余地を消しておくのが、堅牢な設計の鉄則です。
—
ドメイン層での活用イメージ
ドメイン駆動設計では、値オブジェクト(Value Object)を定義します。先ほどのブランド型を応用して、コンストラクタでバリデーションを行うのが実務の定石です。
type PositiveNumber = number & { readonly __brand: ‘PositiveNumber’ };
function createPositiveNumber(value: number): PositiveNumber {
if (value <= 0) {
throw new Error("値は正数である必要があります");
}
return value as PositiveNumber;
}
// 実行時にバリデーションを行い、型で安全を保証する
const price = createPositiveNumber(100);
このように、「生成時にだけバリデーションを通し、以降は型が正しさを保証する」という設計は、アプリケーションの信頼性を劇的に向上させます。
—
最後に:型は「ドキュメント」以上の存在
TypeScriptの型は、単なるコード補完のためのツールではありません。それは、あなたが定義したドメインモデルを、コンパイラという「超高速な自動テスト」で守り抜くための最強の設計図です。
「ただのstring」で済ませるのではなく、そこにあなたのビジネスドメインの魂を吹き込んでください。この小さな工夫の積み重ねが、半年後のあなた自身を、そしてチームメイトを救うことになります。
ここをマスターできれば、あなたはもうTypeScriptの中級者を超え、アーキテクトとしての視座を手に入れています。さあ、次はどんな型を定義しましょうか?