【実務・中級編】Interfaceの「拡張」と「合成」:コンポジション指向の型設計 – TypeScript コア・型システムの基礎解析バイブル

Interfaceの「拡張」と「合成」:コンポジション指向の型設計でTypeScriptを極める

コードレビューをしていて、最もよく見かけるアンチパターンの一つが「巨大でモノリシックなインターフェースの爆誕」だ。

「とりあえずこの画面で必要なデータを全部入れました」と言わんばかりに、50行もある `User` インターフェース。APIのレスポンスが変わるたびにオプションプロパティ(`?`)が増え続け、気づけばどのコンテキストで何が必須なのか誰にも分からない「バグの温床」と化している。

OOP(オブジェクト指向プログラミング)の古い呪縛に囚われたエンジニアは、なんでもかんでも `extends` による「継承の階層」を作ろうとする。しかし、変化の激しいフロントエンド開発や複雑なAPI連携において、深すぎる継承ツリーは毒だ。

TypeScriptの型システムは、 nominal(公称型)ではなく structural(構造的型付け) をベースにしている。この特性を最大限に活かし、「小さなインターフェースを組み合わせる(コンポジション)」 ことこそが、変更に強く、コンパイルも高速な堅牢な型設計の唯一の解である。

今回は、実務の現場で即座に応用できる「インターフェースの拡張と合成」の極意を、テクニカルリードの視点から授けよう。

—

1. なぜ `extends` の多用グセを捨てなければならないのか?

`interface AdminUser extends User, Permissions, AuditLog` のように、`extends` を重ねていくアプローチには構造的な限界がある。

1. 結合度の爆発: 親が変更されたときの子への影響範囲が追えなくなる。
2. 型の肥大化: 「あるコンポーネントでは一部のプロパティしか使わない」というケースでも、無駄に大きな型を持ち回ることになり、IDEの補完候補がノイズまみれになる。
3. 型の衝突: 異なる親の間で同名プロパティの型が競合した際、コンパイラが意図しないエラーや `never` 型を引き起こす。

我々が目指すべきは、継承による「階層構造」ではなく、交差型(Intersection Types: `&`)やインターフェースの宣言結合、そしてユーティリティ型を駆使した「合成(Composition)」である。

—

2. 実践:コンポジション指向による堅牢な型設計

ここでは、実務で頻出する「非同期API連携を伴うユーザー管理・権限管理ダッシュボード」を題材にする。
小さく単一責任に徹したインターフェースを定義し、それらを組み合わせてドメインモデルを構築するプロダクションコードを見てほしい。

// ==========================================
// 1. アトミックな基本インターフェース群(単一責任の原則)
// ==========================================

export interface Identifiable {
readonly id: string;
}

export interface Timestamped {
readonly createdAt: string; // ISO 8601
readonly updatedAt: string;
}

export interface PersonInfo {
firstName: string;
lastName: string;
email: string;
}

export type UserRole = ‘admin’ | ‘editor’ | ‘viewer’;

export interface Authorizable {
role: UserRole;
permissions: readonly string[];
}

// ==========================================
// 2. 「合成(Composition)」によるドメインモデルの構築
// ==========================================

/

  • データベース上の完全なユーザーエンティティ
  • interfaceの拡張(extends)も、基本単位が小さければ安全に機能する

/
export interface UserEntity extends Identifiable, Timestamped, PersonInfo, Authorizable {
isActive: boolean;
}

// ==========================================
// 3. APIリクエスト/レスポンスに特化した型合成
// ==========================================

/

  • 新規作成リクエスト用型
  • IDやタイムスタンプ、システム制御値はクライアントが送るべきではないため除外する

/
export type CreateUserPayload = Omit & {
email: string;
// パスワードは機密情報として独立させる
passwordHash: string;
initialRole: UserRole;
};

/

  • フロントエンドのUIコンポーネント(プロフィールカード)で必要な最小限の型
  • 「必要なものだけを抽出する」というコンポジション指向の真骨頂

/
export type UserProfileCardProps = Identifiable & Pick & {
avatarUrl?: string;
};

この設計が美しい理由

  • 関心事の分離: `Identifiable` や `Timestamped` といった横断的関心事(Cross-cutting concerns)をモジュール化しているため、他のエンティティ(例えば `PostEntity` や `CommentEntity`)でもそのまま再利用できる。
  • イミュータビリティの担保: `Identifiable` や `Timestamped` のプロパティに `readonly` を付与することで、不変性を型レベルで強制している。これにより、アプリケーション層での不意のミューテーションをコンパイル時に検知できる。

—

3. 拡張(`extends`)と合成(`&`)の正しい使い分け

TypeScriptにおいて、`interface A extends B` と、型エイリアスによる `type A = B & C` は一見似ているが、コンパイラの振る舞いとエラーメッセージの親切さに決定的な違いがある。

① インターフェースの拡張 (`extends`)

  • 用途: オブジェクトの形状に明確な「階層」や「IS-A関係」がある場合。
  • メリット: コンパイラがプロパティの互換性をあらかじめチェックするため、IDEのエラーメッセージが極めて読みやすい。また、「宣言結合(Declaration Merging)」が使えるのはインターフェースだけだ。

// 宣言結合の例:サードパーティの型を拡張する場合などに有効
interface UserEntity {
metadata?: Record;
}

② 交差型による合成 (`&`)

  • 用途: 既存の型をスライスし、組み合わせて新しい型を即座に作りたい場合(ユーティリティ的なアプローチ)。
  • 注意点: プリミティブ型同士を結合すると `never` になるなど、型の衝突時にエラーが難解になりがちだが、オブジェクトの合成においては極めて強力。

// 異なるAPIレスポンスの合体
type ApiResponse = T & {
status: 200 | 400 | 500;
traceId: string;
};

type UserApiResponse = ApiResponse;

シニアの知見:
「公開APIやライブラリの境界線(Public API Boundary)」では拡張しやすい `interface` を基軸にし、コンポーネントのPropsや関数の引数といった「局所的なスコープ」では `type` と交差型・ユーティリティ型を駆使して柔軟に合成する。これが最もスケールするアプローチだ。

—

4. パフォーマンスとコンパイラ負荷への配慮

TypeScriptの型システムはチューリング完全であるため、複雑な型パズルや無限にネストした交差型は、TypeScript言語サーバー(tsserver)のパフォーマンスを直撃する。

型定義が肥大化・複雑化すると、VS Codeの補完(IntelliSense)が重くなり、CIでの `tsc –noEmit` のビルド時間が数分に跳ね上がる原因になる。

パフォーマンスを悪化させるアンチパターン

// ❌ 悪い例:過剰にネストされた交差型とユーティリティの乱用
type ComplexType = Omit & Pick & {
nested: F extends { kind: infer U } ? U : never;
};

改善のプラクティス

1. 型エイリアスに名前を付ける(Named Types): 複雑な交差型をインラインで書かず、意味のある名前を付けた型エイリアスとしてキャッシュさせる。
2. プリミティブな型評価を心がける: 条件付き型(Conditional Types)の深すぎる再帰は避け、フラットな合成を意識する。
3. `readonly` とプレーンなオブジェクトの維持: 不要な `Partial` や `Required` の全域適用を避け、必要な部分のみを絞り込む。

—

5. まとめ:型は「ドキュメント」であり「守護神」である

優れた型設計とは、コードを書く手間を増やすものではなく、「未来の自分やチームメンバーへの最高のドキュメント」であり、リファクタリングの恐怖を消し去る「最強の守護神」である。

モノリシックなインターフェースを捨て、アトミックな部品を組み合わせる「コンポジション指向」を取り入れることで、あなたのコードベースは驚くほど軽快になり、変更に対してしなやかに耐える強靭さを手に入れるだろう。

今日のレビューから、その巨大な `interface` を解体してみてはどうかね?

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