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

プリミティブへの決別:TypeScriptでドメイン駆動設計を「型」に刻む

多くの開発者が、TypeScriptの `string` や `number` を「ただの型」として放置している。これがバグの温床だ。

「ユーザーIDも、商品IDも、ただの `string` である」という設計は、コンパイラを盲目にする。`UserId` を期待する関数に、誤って `ProductId` を渡してもコンパイルが通ってしまう――これはTypeScriptを使っている意味を半分捨てているに等しい。

真のアーキテクトは、「プリミティブな型にドメインの文脈(コンテキスト)を付与する」ことで、コンパイル時にバグを撲滅する。今日は、ブランド型(Branded Types)を用いて、DDDの「値オブジェクト(Value Object)」をTSの型システム上でいかに美しく、かつゼロコストで実現するかを伝授する。

—

なぜ「型」だけでドメインを表現するのか

DDDにおいて値オブジェクトは「等価性」と「不変性」を保証する重要な概念だ。しかし、クラスとして実装すると実行時のメモリ消費やインスタンス化のコストが発生する。

TypeScriptのブランド型(Branded Types)は、コンパイル時のみに作用する「メタデータ」だ。実行時はただのプリミティブとして振る舞うため、パフォーマンスへの悪影響は皆無。それでいて、型システム上では別物として厳格に扱われる。

実践:ブランド型によるドメイン表現

まずは、最も汎用的で堅牢なブランド型の定義を見てほしい。

/

  • ブランド型の汎用ユーティリティ
  • T: 元の型, B: ブランド識別子

/
type Brand = T & { readonly __brand: B };

// ドメインモデルの定義
type UserId = Brand;
type OrderId = Brand;

// ファクトリー関数(コンストラクタ的な役割)
const createUserId = (id: string): UserId => id as UserId;
const createOrderId = (id: string): OrderId => id as OrderId;

// 利用例
function processUser(id: UserId) {
console.log(`Processing user: ${id}`);
}

const userId = createUserId(‘user_123’);
const orderId = createOrderId(‘order_999’);

processUser(userId); // OK
// processUser(orderId); // ❌ コンパイルエラー: Type ‘OrderId’ is not assignable to ‘UserId’

このコードの美しさは、「実行時には単なる文字列なのに、コンパイラには別物として映る」という点にある。`__brand` プロパティは実行時には存在せず、コンパイル時にのみ型チェックを強制するゲートキーパーとして機能する。

—

実務で差が出る:バリデーションと型の融合

値オブジェクトの真価は「不正な値がドメイン層に侵入することを防ぐ」点にある。型定義とバリデーションをセットで管理するパターンが、プロダクションコードでは定石となる。

/

  • Email型の定義(バリデーション付き)

/
type Email = Brand;

class DomainError extends Error { name = ‘DomainError’; }

function createEmail(input: string): Email {
// ここにドメインルールを集中させる
if (!input.includes(‘@’)) {
throw new DomainError(‘Invalid email format’);
}
return input as Email;
}

// 非同期API連携での活用
async function fetchUserEmail(userId: UserId): Promise {
const response = await fetch(`/api/users/${userId}/email`);
const data = await response.json();

// 外部からのデータは必ずここで型を保証する(境界線での防衛)
return createEmail(data.email);
}

この設計により、「型定義を見れば、その値がどのようなバリデーションを通過したか」が一目でわかるようになる。APIレスポンスの境界で型変換を行うだけで、その後のビジネスロジック内では `string` の不正なフォーマットに怯える必要は一切なくなる。

—

パフォーマンスとアーキテクチャの極意

多くのエンジニアが懸念する「実行時のパフォーマンス」だが、ブランド型はTypeScriptのイレイズ(型消去)の恩恵を最大限に受ける。

  • Zero Overhead: コンパイル時に型チェックが行われた後は、`__brand` プロパティは消滅する。`class` を使う場合のようなメモリ消費やメソッドのバインドコストは存在しない。
  • 型推論の破壊を防ぐ: ブランド型はプリミティブと互換性があるため、JSONへのシリアライズや、既存のライブラリ(`axios`, `react-hook-form` など)との親和性が非常に高い。

アーキテクトからのアドバイス

1. ドメインの境界にブランドを置け: DTO(APIの入出力)からドメインモデル(ビジネスロジック)へ渡す際に、必ず `createXxx` のようなファクトリーを通すこと。ここが唯一の「型汚染」ポイントであり、同時に「防衛ポイント」だ。
2. 無理にクラス化しない: 複雑なメソッドを持つ値オブジェクト以外は、ブランド型で済ませるのがTS流の賢いDDDだ。
3. チームへの浸透: `brand` 型を使うことで「この関数には適当な文字列を渡してはいけない」という意思がコードから伝わるようになる。これは強力なドキュメント代わりになる。

TypeScriptは、単なるJavaScriptの型補完ツールではない。コンパイラの力を借りて、ビジネスロジックの「意味」をコードに刻み込むための最強の武器だ。今日から `string` や `number` で溢れかえったコードに、ブランドという名の「意思」を刻んでほしい。それが、バグのない堅牢なプロダクトへの唯一の近道だ。

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