【実務・中級編】Interfaceの「継承」と「合成」:コンポジション指向で再利用可能な型パーツを作る – TypeScript コア・型システムの基礎解析バイブル

TypeScriptの「継承」という罠を捨てろ:コンポジションで構築する堅牢な型システム

コードレビューをしていると、未だに `extends` を使った巨大なインターフェースの階層構造を見かける。`BaseUser` を継承し、`AdminUser` を作り、さらにそれを継承して `SuperAdminUser` を作る。

その設計、数ヶ月後に破綻するぞ。

TypeScriptにおいて、`interface` の継承は「is-a(〜である)」という強い依存関係を強制する。しかし、現実のフロントエンド開発で必要なのは「has-a(〜を持つ)」という柔軟な能力の付与だ。この記事では、継承による硬直化を避け、コンポジション(合成)によって保守性の高い型を作るための「型設計の極意」を伝授する。

—

なぜ「継承」は型システムのアンチパターンなのか

継承の最大の罪は、「不必要なプロパティの混入」と「循環参照の誘発」だ。

例えば、APIから取得したユーザー情報に「認証機能」を追加したいとき、継承を使うと以下のようになる。

// 継承の悪例
interface User { id: string; name: string; }
interface AuthenticatedUser extends User { token: string; }
interface AdminUser extends AuthenticatedUser { permissions: string[]; }

一見綺麗だが、`AdminUser` を使う場面で、APIのレスポンスには存在しない `token` や `permissions` が型定義上は「必須」として扱われる。結果、モックデータを作る際に余計なプロパティを埋めるハメになり、テストコードが肥大化する。

継承は「型を広げる」作業だが、コンポジションは「型を小さく組み合わせる」作業だ。前者は負債を生み、後者は資産を生む。

—

コンポジション指向:Intersection Typesを活用した「型パーツ」設計

TypeScriptの強力な武器は `&`(Intersection Types)だ。これを使えば、ドメインロジックを最小単位の「パーツ」に分割できる。

1. 最小単位の型パーツ(Atomic Types)を定義する

まずは、再利用可能な最小単位を定義する。

// 型パーツ(合成の素材)
type Identifiable = { id: string };
type Timestampable = { createdAt: Date; updatedAt: Date };
type AuthInfo = { token: string; expiresAt: number };
type UserProfile = { name: string; email: string };

2. 合成による目的別の型構築

次に、これらを組み合わせて具体的な「機能」を作る。

// コンポジションによる型構築
// ユーザーの基本情報と識別子を合成
type User = Identifiable & UserProfile;

// 認証が必要な場面でのみ、AuthInfoをミックスインする
type AuthenticatedUser = User & AuthInfo;

// 管理画面用の拡張
type AdminUser = User & { role: ‘admin’ | ‘superadmin’ };

このアプローチの利点は、「必要な型だけをその場で合成できる」ことだ。`AuthenticatedUser` を定義するために `User` を継承する必要はない。`User` 型の定義に手を加えずに、外部から能力を付与できる。これがコンポジションの神髄だ。

—

実務で差が出る「Mapped Types」と「Utility Types」の融合

さらに一歩進んで、非同期API連携の現場でよくある「部分更新(Partial Update)」を美しく処理する設計を紹介しよう。

// 読み取り専用のIDを除外した更新用型を自動生成
type UpdatableProfile = Omit & {
// 必要であれば更新用の制限をかける
readonly updatedAt: Date;
};

// ユーティリティ関数での活用
function updateProfile(
entity: T,
updates: Partial>
): T {
return { …entity, …updates };
}

この設計の強みは、「型定義を変更した際の影響範囲が局所的である」ことだ。継承関係が深ければ、親を書き換えるたびに子全員が爆発するが、合成であれば個別の型パーツを調整するだけで済む。

—

パフォーマンス上の注意:型評価の重さ

最後に一点、アーキテクトとしての警告だ。

TypeScriptの型評価は、複雑な `Conditional Types` や `Mapped Types` を多用しすぎると、IDEのレスポンスが極端に低下する。特に `extends` を使った深い継承ツリーは、コンパイラが型を解決する際の探索コストを増大させる。

「合成はシンプルに」を心がけよ。

  • 3段以上の継承は禁止せよ。
  • インターフェースを `&` で連結するのは2〜3個までにとどめよ。
  • それ以上必要なら、それは「型」ではなく「クラス」や「コンポーネント」の責務分離を検討すべきサインだ。

—

結論:型は「階層」ではなく「地図」である

継承は、型を「ツリー構造」に閉じ込める。しかし、複雑なWebアプリケーションにおいて、型はもっと動的で自由であるべきだ。

コンポジションで小さなパーツを組み合わせることは、「どの型が、どの能力を持っているか」という関係性を地図のように記述することに他ならない。

明日からのコードレビューでは、`extends` を見かけたら「これは合成に置き換えられないか?」と自問してみてほしい。その瞬間から、あなたの書くTypeScriptは、単なる静的解析のための記述から、プロダクトの意図を正確に伝える「ドキュメント」へと進化するはずだ。

型を掌握する者が、複雑なフロントエンドを支配する。健闘を祈る。

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