巨大なInterfaceという「技術的負債」を殺せ:コンポジションで実現する堅牢な型設計
TypeScriptで開発をしていると、必ず直面する「肥大化したInterface」という怪物。
`interface User` が50行を超え、`UserForm`、`UserDetail`、`UserApiRequest` と似たような型が乱立し、修正のたびに全箇所を追う羽目になる。
多くのエンジニアはここで「継承(extends)」を使おうとします。しかし、安易な継承は型定義をカオスに陥れる元凶です。今日は、「継承ではなく合成(Composition)」という観点から、TypeScriptの型システムを再構築する方法を伝授します。
—
なぜ「継承」は罠なのか
継承は「is-a(〜である)」関係を定義しますが、フロントエンドのコンポーネント設計において、この関係はしばしば破綻します。
- 脆弱な結合: 親の定義を変更すると、予期せぬ場所まで型エラーが伝播する。
- 継承の深さ: 階層が深くなると、`tsc` が型の評価(Type Instantiation)を解決する際の計算コストが跳ね上がり、エディタの補完が重くなる。
- 柔軟性の欠如: 複数の親を持つことができないため、機能が混ざり合うと「怪物のような親クラス」が生まれる。
これからは、「小さなパーツを作り、それを組み合わせる(Composition)」設計にシフトしましょう。
—
実践:コンポジションによる型設計パターン
APIレスポンスの管理を例に、具体的なコードを見ていきましょう。
1. 小さな型パーツ(Atomic Types)の定義
まずは、再利用性の高い最小単位の型を定義します。
// 識別子に関するパーツ
type IdIdentity = { id: string };
type Timestamp = { createdAt: string; updatedAt: string };
// ユーザー属性に関するパーツ
type UserProfile = { username: string; email: string };
type UserAuth = { role: ‘admin’ | ‘user’; isActive: boolean };
2. Intersection Types で合成する
これらを組み合わせて、ドメインに即した型を構築します。
// データベース上の完全なユーザー型
export type User = IdIdentity & UserProfile & UserAuth & Timestamp;
// APIリクエスト用(idやタイムスタンプは不要)
export type CreateUserRequest = UserProfile & { password: string };
// 特定の属性のみ必要なコンポーネント用
export type UserSummary = IdIdentity & UserProfile;
ここでのポイント:
`type` を使うことで、`&` (Intersection) による合成が自然に行えます。これにより、コンポーネントに必要なプロパティだけを抽出する「型レベルの合成」が可能になります。
—
現場で役立つ「高度な合成テクニック」
ユーティリティ型を用いた「差分」の抽出
単にパーツを合成するだけでなく、既存の型から不要なものを削ぎ落とすのも合成の作法です。
// 既存のUser型から、UI表示に不要な機密情報を除外して合成
export type UserDisplay = Omit
lastLogin: string;
};
インターフェースの「プロパティオーバーライド」
もしInterfaceを使う場合でも、継承を避けて `Pick` や `Omit` を活用しましょう。
interface BaseProps {
id: string;
className?: string;
}
// 継承せず、必要なものを合成する
interface ButtonProps extends BaseProps {
label: string;
onClick: () => void;
}
—
パフォーマンスと保守性のための注意点
1. 型評価のコストを意識する
複雑な `Conditional Types` や `Mapped Types` を多用しすぎると、TypeScriptコンパイラは膨大な計算を強いられます。
- 名前付き型を優先: インラインで複雑な合成を行うと、エラーメッセージが `(Id & Profile) & (Auth & Timestamp)` のように地獄のような表示になります。必ず名前(`type …`)を付けてください。
2. 「合成の深さ」を制限する
3階層以上の合成は、型推論の可読性を著しく下げます。フラットな合成を心がけてください。
—
結論:型は「ドキュメント」である
TypeScriptの型システムは、単なるバリデーションツールではありません。「このデータの構造はどうあるべきか」という設計図です。
継承でガチガチに固めた巨大なInterfaceは、変更に対する拒絶反応を示します。しかし、小さなパーツを合成する設計は、要件の変化に対して「パーツを入れ替えるだけ」という柔軟性をもたらします。
今日から、あなたのプロジェクトの `types.ts` を開いてください。`extends` の文字を消し、`&` や `Pick/Omit` を使って型を「組み立てる」意識に変えるのです。その瞬間から、あなたのコードベースは圧倒的にメンテナンスしやすくなるはずです。
設計は、引き算から始まる。
最高のプロダクトには、常に研ぎ澄まされたシンプルな型が存在します。