【実務・中級編】Interfaceの構造的部分型が引き起こす「予期せぬ代入」の防ぎ方 – TypeScript コア・型システムの基礎解析バイブル

構造的部分型(Structural Subtyping)という名の両刃の剣

TypeScriptの最も強力であり、同時に最も危険な特性――それは「構造的部分型(Structural Subtyping)」です。

JavaやC#といった公称型(Nominal Typing)の言語に慣れ親しんだエンジニアほど、この罠に足元をすくわれます。TypeScriptでは、オブジェクトの「名前」ではなく「形(プロパティの構造)」だけで型の互換性が判定されます。

interface UserID {
id: string;
}

interface OrderID {
id: string;
}

function processOrder(orderId: OrderID) {
// …
}

const userId: UserID = { id: “usr_123” };

// 【悲劇】コンパイルエラーにならない!
processOrder(userId);

実務の現場で、`UserID`を渡すべき箇所にうっかり`OrderID`を渡し、そのままバックエンドのデータベースへクエリを発行して大惨事になった……というインシデントに心当たりはないでしょうか。コンパイラは「どちらも `id: string` を持っているから同じ構造だ」と判断し、これをスルーします。

今回は、このインターフェースの緩い互換性を完全に封じ込め、「コンパイル時に型の意味を厳密に強制する」ためのブランド型(Branded Types)を用いた極限の設計パターンを解説します。

—

なぜ通常のInterfaceでは防げないのか?

フロントエンドのアプリケーションが複雑化し、APIクライアント、状態管理、コンポーネントのPropsが交錯する現代において、プリミティブ型(`string`や`number`)の乱用はバグの温床です。

特に以下のようなドメイン駆動設計(DDD)的なID管理や、ステータス管理において、構造的部分型は牙を剥きます。

  • ユーザーIDとテナントIDの混同
  • 金額(円)と数量の混同
  • アクティブなURL文字列と生のエスケープ前文字列の混同

これらを防ぐために、ランタイムのオーバーヘッドを一切生じさせずに、コンパイラのみに厳格な規律を強制するアプローチを見ていきましょう。

—

解決策:ブランド型(Branded Types)による公称型のエミュレーション

TypeScriptの型システム上で「構造」に「目印(ブランド)」を付与し、他の構造からの代入をコンパイルエラーにするテクニックがブランド型です。

以下のプロダクションコードを見てください。実務のコードベースにそのまま組み込める堅牢な設計です。

/

  • 汎用的なブランド型生成ヘルパー
  • ランタイムのメモリを一切消費せず、型システムの上だけで「公称型」をエミュレーションする

/
declare const __brand: unique symbol;

type Brand = T & {
readonly [__brand]: TBrand;
};

/ ==========================================

  • ドメイン固有の厳密な型定義
  • ========================================== /

// ユーザーを一意に特定するID
type UserId = Brand;

// 注文を一意に特定するID
type OrderId = Brand;

// 割引前の素の価格(円)
type RawPrice = Brand;

/ ==========================================

  • 型安全なファクトリー関数(スマートコンストラクター)
  • ========================================== /

/

  • 生の文字列から安全な UserId を生成する
  • ここでバリデーション(UUIDの形式チェック等)を挟むのが正しい設計

/
function createUserId(id: string): UserId {
if (!id.startsWith(“usr_”)) {
throw new Error(“Invalid UserId format.”);
}
return id as UserId;
}

function createOrderId(id: string): OrderId {
if (!id.startsWith(“ord_”)) {
throw new Error(“Invalid OrderId format.”);
}
return id as OrderId;
}

/ ==========================================

  • 実践:コンパイルエラーによる事故防止
  • ========================================== /

function shipOrder(orderId: OrderId, customerId: UserId): void {
console.log(`Shipping order ${orderId} to customer ${customerId}`);
}

// — 正常系 —
const validOrder = createOrderId(“ord_999”);
const validUser = createUserId(“usr_42”);

shipOrder(validOrder, validUser); // ✨ 完璧にコンパイルを通過

// — 異常系(コンパイルエラーで即座に検知!) —
// shipOrder(validUser, validOrder);
// ❌ コンパイルエラー:
// ‘UserId’ 型の引数を ‘OrderId’ 型のパラメータに割り当てることはできません。

// — プレミティブの直接代入もブロック —
// shipOrder(“ord_999”, “usr_42”);
// ❌ コンパイルエラー:
// ‘string’ 型は ‘Brand‘ 型に割り当てられません。

このコードが優れている理由

1. ゼロ・ランタイムコスト: `unique symbol` と交差型(Intersection Types)は、TypeScriptがトランスパイル(JSへの変換)を行う際に完全に消去されます。実行時のメモリ消費やパフォーマンス低下はゼロです。
2. イミュータビリティの強制: `readonly [__brand]: TBrand` と定義することで、誤ってブランドプロパティを書き換えるバグを防ぎます。
3. スマートコンストラクターの強制: 生のプリミティブからブランド型を作る箇所(`createUserId`等)を強制するため、バリデーション漏れを防ぎ、ドメインの整合性が保証されたデータだけがアプリケーション内部に流通します。

—

フロントエンド・コンポーネント設計への応用

このパターンは、Reactなどのコンポーネント設計や、APIレスポンスの型定義でも絶大な効果を発揮します。

例えば、HTMLの``要素に渡す「サニタイズ済みのHTML文字列」と「未サニタイズのユーザー入力文字列」を型レベルで厳密に区別する場合です。

type SanitizedHtml = Brand;
type RawUserInput = Brand;

interface SafeHtmlRendererProps {
// 危険な生文字列の混入を型レベルで完全に防ぐ
content: SanitizedHtml;
}

// コンポーネント側
function SafeHtmlRenderer({ content }: SafeHtmlRendererProps) {
// dangerouslySetInnerHTML に渡す前に、型で安全性が担保されている
return

;
}

「うっかりサニタイズ前の文字列をそのままJSXに渡してXSS脆弱性を生んでしまった」という致命的なヒューマンエラーを、CI/CDパイプライン上の `tsc` が未然に防いでくれます。

—

パフォーマンスと開発体験(DX)上の注意点

ブランド型は非常に強力ですが、コードベース全体に適用する際にはいくつかのインフラ的配慮が必要です。

1. サードパーティライブラリとの境界線
APIから返ってきたJSONや、外部ライブラリのオブジェクトはブランドを持っていません。そのため、APIクライアント層(データフェッチ層)の境界で、必ずアサーションやファクトリー関数を通す「境界防御(Boundary Defense)」を構築する必要があります。アプリケーションの内部ロジックへ生データを侵入させないことが鉄則です。

2. 型定義の肥大化に注意
あらゆるプリミティブにブランドを付与すると、モックデータを作るときやテストコードを書く際にキャスト(`as UserId` 等)が増えすぎて開発体験(DX)が低下します。

  • 「IDや金額など、間違えたときに致命的なドメイン知識」
  • 「単なるフラグや一般的な文字列」

を明確に切り分け、コストとベネフィットのバランスを見極めて導入してください。

—

チーフアーキテクトからの提言

「TypeScriptの型はガバガバだ。どうせコンパイル後はただのJavaScriptになるのだから」と諦めていませんか?

それは違います。TypeScriptの型システムは、開発者の意図をコンパイラに伝えるための「言語」であり、チーム全体の共通認識をコードに定着させるための最強のアーキテクチャツールです。

今回紹介したブランド型を取り入れることで、「間違ったコードは、そもそもコンパイルが通らない」という理想的な開発環境を手に入れることができます。今日のコードレビューから、曖昧な `string` 型の引数を撲滅し、型による堅牢なドメインモデリングを実践していきましょう。

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