【テクニカル・上級編】Type Aliasによる「ドメイン駆動設計(DDD)」の型付け:値オブジェクトの表現 – TypeScript コア・型システムの基礎解析バイブル

型の「形」を解体する:ドメイン駆動設計におけるBranded Typesの深淵

TypeScriptの型システムは、その本質において「集合論」と「構造的部分型(Structural Subtyping)」の融合である。多くの開発者は`interface`と`type`の使い分けで悩むが、アーキテクトの視点から言えば、それは「静的解析のレイヤーで何をガードするか」という防衛戦略の差異に過ぎない。

DDD(ドメイン駆動設計)において、プリミティブな`string`や`number`をそのままドメインモデルに混入させることは、型安全性を放棄し、ランタイムでのバグを誘発する「型汚染」そのものである。

本稿では、TypeScriptのコンパイル時メタデータと、ランタイムにおけるメモリ表現を掌握するための「Branded Types」を用いた最強の型戦術を紐解く。

—

1. 構造的部分型の罠を突破する:ブランド化の正体

TypeScriptは「同じ形を持つものは同じ型である」と見なす。これが構造的部分型の本質だ。しかし、ドメイン層では`UserId`と`OrderId`を混同してはならない。

ここで登場するのが「ブランド化(Branding)」である。

// コンパイラにのみ存在する幽霊的なプロパティ「__brand」
type Brand = K & { readonly __brand: T };

type UserId = Brand;
type OrderId = Brand;

function getOrder(id: OrderId) { / … / }

const rawId = “user_123” as UserId;
// getOrder(rawId); // ❌ Error: Property ‘__brand’ is missing

この手法は、コンパイル時にのみ型チェッカーを欺くための「壁」を構築する。特筆すべきは、コンパイル後のJavaScriptにはこのオブジェクトの変更や型情報は一切残らないという点だ。V8エンジンは、これがただの`string`としてメモリ上のヒープに配置されるため、パフォーマンスのオーバーヘッドはゼロである。

—

2. コンパイル時評価とメモリ安全性

シニアエンジニアが懸念すべきは、複雑な型変換がコンパイル速度を低下させることではない。「型定義がランタイムのメモリレイアウトとどれだけ乖離しているか」だ。

DDDにおける値オブジェクト(Value Object)を`class`で実装すると、各インスタンスが`prototype`を持ち、メモリ効率が悪化する。一方、Branded Typesを用いた手法なら、プリミティブ型としての挙動を維持しつつ、ガード節を通した瞬間だけ「ドメインとしての正当性」を付与できる。

安全なコンストラクタの強制

`as`キャストは悪である。コンパイル時の防御壁を突破するには、スマートコンストラクタを介したランタイム検査を必須とする。

// 実行時のバリデーションとブランド化の同期
function createUserId(value: string): UserId {
if (!/^[a-z0-9-]+$/.test(value)) {
throw new Error(‘Invalid UserId format’);
}
return value as UserId;
}

このパターンを採用することで、型システムは「値が正しい形式である」という数学的証明を、境界(Boundary)を通過するたびに再計算する。これにより、システム全体における「不正な状態の伝播」を論理的に遮断できる。

—

3. イベントループと型ガードの相関

Node.jsのイベントループにおいて、非同期タスクがキューに積まれる際、オブジェクトの構造が変わることはない。しかし、非同期処理の合間に外部からのデータが注入される場合、その型が「未検証のプリミティブ」であることは最大の脆弱性となる。

ここで、`Branded Types`と`Discriminated Unions`を組み合わせることで、イベントループのどのフェーズにおいても「このデータは検証済みか否か」を型で明示できる。

type ValidatedUser = { status: ‘validated’; id: UserId };
type PendingUser = { status: ‘pending’; id: string };

type UserEvent = ValidatedUser | PendingUser;

// イベントループを消費するハンドラ
async function handleUserUpdate(event: UserEvent) {
if (event.status === ‘pending’) {
// ここではまだUserId型ではない(ガードが必要)
return;
}
// このブロック内では確実に UserId が保証される
processInternal(event.id);
}

—

4. アーキテクトとしての結論:型の「重み」を制御せよ

TypeScriptを掌握するとは、「いつ、どのタイミングで型チェックを強制し、どのタイミングでランタイムの速度を優先するか」を制御することに他ならない。

1. 境界層(Infrastructure/API Layer): `unknown`から`Branded Type`へ昇格させる。ここで全バリデーションを完了させる。
2. ドメイン層(Domain Layer): `Branded Type`を用いて純粋なビジネスロジックを記述する。ここではキャストを禁止し、型の整合性のみを信じる。
3. 表示層(UI Layer): 必要な場合にのみ`string`へ展開する。

このアーキテクチャは、コード量こそ増えるように見えるが、長期的には「なぜこの値がここで不正なのか?」というデバッグの泥沼を回避する、最も安価な保険となる。

型システムは、単なる静的解析ツールではない。それは、複雑なドメインの境界を定義し、ランタイムの狂気を封じ込めるための、最も強力な防壁である。その壁をどう設計するか。それこそが、我々アーキテクトに課せられた責務だ。

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