最近、Branded Typesというものを知った
Branded TypesというDDDに使える方法があるのを知り、調べたところBranded Typesを説明した記事が沢山あった(勉強になります)
- TypeScript の型安全性を高める Branded Types
- Branded Typesを導入してみる / TypeScript一人カレンダー
- Branded Type について理解する
-
DDD(ドメイン駆動設計)と関数型ドメインモデリングはカオスなDB設計を救えるか?
...and more
ここでは、DDDの実践におけるClassを使ったものとBranded Typesを使ったものの違いについて調査、まとめていく
あくまで自分用に調査したものなので、経験に基づくものではありません
Branded Types とは
- TypeScriptのBranded Types(ブランド付き型)とは、既存の型に特別な「ブランド」を付けることで、型システム上で区別できるようにする方法のこと
- 実行時にはオーバーヘッドなしに、コンパイル時の型安全性を向上させることができる
基本概念
- Branded Typesは、基本的な型(string、numberなど)に、実行時には存在しない特別なプロパティを追加して作成する
- 上記のプロパティは型チェックの際にのみ使用され、異なる意味を持つ同じ基本型を区別するために役立つ
構造的型システムの限界
-
TypeScriptは構造的型システム(structural typing)を採用しており、型の互換性は構造(プロパティや戻り値の型など)によって決まる
-
構造が同じであれば異なる意味を持つ型でも互換性があるとみなされてしまう
typescripttype UserId = string; type PostId = string; function getUser(id: UserId) { /* ... */ } const postId: PostId = "post-123"; getUser(postId); // ❌ エラーにならない!
Branded Typesの実装例
typescript
// 汎用的なBranded Typesの定義
type Brand<T, B> = T & { __brand: B }
// EmailAddress型の例
type EmailAddress = Branded<string, 'EmailAddress'>;
function createEmailAddress(value: string): Branded {
if(value.includes("@")) throw new Error('Invalid Email');
return value as EmailAddress;
}
Classを使った時との違い
トレードオフ
Branded Types | Classベース | |
---|---|---|
学習コスト | TypeScript型システムの深い理解が必要 | オブジェクト指向の基本概念で対応可 |
拡張性 | プリミティブ拡張に限定 | 柔軟なクラス階層構築可能 |
保守性 | 型定義の集中管理が必要 | 設計パターンに沿った整理が可能 |
ユースケース比較
項目 | Branded Types | Classベース |
---|---|---|
単純な値の区別 | ◎(通貨単位、ID種別) | △ |
複雑なビジネスルール | △ | ◎(割引計算、適用条件) |
大規模チーム開発 | ○(型安全性重視) | ◎(オブジェクト指向慣習) |
パフォーマンスクリティカル | ◎ | △ |
ドメイン表現力
-
Branded Types: プリミティブ値の意味的差異を表現するのに優れる
typescript
type Email = Branded<string, 'Email'>; type Phone = Branded<string, 'Phone'>;
-
Class: 複雑なビジネスルールをメソッドとしてカプセル化可能
typescript
class BankAccount { private balance: number; constructor(initialBalance: number) { if (initialBalance < 0) { throw new InvalidOperationException("初期残高は0以上である必要があります"); } this.balance = initialBalance; // 初期残高を設定 } deposit(amount: number): void { if (amount <= 0) { throw new InvalidOperationException("預金額は正の数である必要があります"); } this.balance += amount; console.log(`¥${amount} を預金しました。現在の残高: ¥${this.balance}`); }
組み合わせるというアプローチも?
- Branded Typesで値オブジェクトを定義しつつ、Classでエンティティを実装するハイブリッドアプローチ
- IDはBranded Type、集約ルートはClassで表現することで、型安全性と振る舞いのカプセル化を両立
typescript
// Branded Type for ID
type UserId = Branded<string, 'User'>;
// Class for Entity
class User {
constructor(
public readonly id: UserId,
private email: Email,
private status: AccountStatus
) {}
changeEmail(newEmail: Email) {
// ビジネスルール実装
}
}