Interfaceの継承 vs Typeの交差型:大規模TSコードベースにおけるパフォーマンスと型エラーの正体
コードレビューをしていて、次のようなコードに出くくしたことはないだろうか。
// よく見るが、実は大規模化すると地雷を踏む書き方
type User = { id: string; name: string };
type Admin = User & { permissions: string[] };
一見すると何の問題もないコードに見える。しかし、これが数万行規模のエンタープライズなコードベースになり、何十ものオブジェクト型が複雑に絡み合い始めると、途端に「型定義ホバー時のレスポンス悪化」「ビルド時間の増大」「エラーメッセージの解読不能(いわゆる `A & B & C & D…` 地獄)」という深刻なパフォーマンスと開発体験の劣化を引き起こす。
今回は、TypeScriptのコアエンジン(tsc)が裏側で型をどう評価・キャッシュしているかというコンパイラ内部の挙動にまで踏み込み、「Interfaceの継承(`extends`)」と「Typeの交差型(`&`)」の決定的な違いをロジカルに解剖していく。
—
1. コンパイラは型をどう見ているか?(内部メカニズムの差)
まず大前提として、TypeScriptにおける `interface` と `type` は単なる「書き方の好み」ではない。コンパイラ(Type Checker)のメモリ上での扱われ方が根本的に異なる。
Interfaceの継承(`extends`): 構造の「結合と事前計算」
Interfaceは、宣言された時点でそのプロパティの形状(Shape)がフラットにキャッシュされやすい。
`interface B extends A` は、「Aというベースがあり、そこにBの差分をマージした新しい単一のインターフェース構造」としてシンボルテーブルに登録される。そのため、IDE(LSP)が補完を引く際や、型チェックを行う際のコストが非常に低い。
Typeの交差型(`&`):遅延評価される「無限のパズル」
一方、交差型 `A & B` は文字通り「AかつBである」という集合論的な制約の合成を意味する。
TypeScriptの型チェッカーは、交差型を評価する際、即座に一つの綺麗なオブジェクト型にまとめるわけではない。多くの場合、「遅延評価(Lazy Evaluation)」され、プロパティにアクセスしたり、別の型に代入しようとした瞬間に、コンパイラは内部で両者のプロパティの競合解決(プリミティブ型同士ならnever化、オブジェクトなら再帰的なマージ)を試みる。
これが何重にもネストしたとき(例: `T1 & T2 & T3 & … & T10`)、コンパイラの計算量は爆発的に増加する。これが、大規模プロジェクトでビルドが遅くなる隠れた主犯格である。
—
2. 開発現場を蝕む「交差型地獄」の恐怖
実務でよくある、APIレスポンスの共通化と拡張のシーンを考えてみよう。
❌ 悪手:交差型(`&`)を多用した設計
type Timestamps = {
createdAt: string;
updatedAt: string;
};
type SoftDeletable = {
deletedAt: string | null;
};
type Entity = {
id: string;
};
// 小悪魔的な誘惑:パッと書けるので乱用されやすい
type User = Entity & Timestamps & {
name: string;
email: string;
};
type AdminUser = User & SoftDeletable & {
permissions: readonly string[];
};
type SuperAdminUser = AdminUser & {
masterKey: string;
};
このコードが1,000ファイルある巨大なモノリスでどう評価されるか。
あるコンポーネントで `SuperAdminUser` のプロパティにアクセスしようとした瞬間、TSの型チェッカーは `Entity & Timestamps & { name: string, email: string } & SoftDeletable & { permissions: readonly string[] } & { masterKey: string }` という巨大な交差型の海を泳ぎ、プロパティの型を解決しなければならない。
その結果、何が起きるか:
1. ホバー情報の爆発: エディタで型にマウスオーバーしたとき、IDEが固まる、あるいは `User & SoftDeletable & …` のような可読性ゼロの型がそのまま表示される。
2. 不親切なエラーメッセージ: 型ミスマッチが起きた際、コンパイラは「どの交差のどのプロパティで矛盾が起きたか」を追いきれず、次のような冷酷なエラーを吐く。
> Type ‘X’ is not assignable to type ‘SuperAdminUser’. The types of ‘permissions’ are incompatible… (以下、数行にわたる複雑な型定義の差分)
—
3. 【推奨】Interfaceの継承によるクリーンかつ高速な設計
では、プロフェッショナルな現場ではどう設計すべきか。答えは 「ベースは Interface で定義し、必要な拡張は `extends` を使う」 ことだ。
⭕ 堅牢解:Interfaceの継承を主軸にした設計
/
- 監査可能なエンティティの基本ベース
/
export interface TimestampedEntity {
readonly id: string;
readonly createdAt: string;
readonly updatedAt: string;
}
/
- 論理削除可能なエンティティのMixin的インターフェース
/
export interface SoftDeletableEntity {
readonly deletedAt: string | null;
}
/
- 基本ユーザー
/
export interface User extends TimestampedEntity {
name: string;
email: string;
}
/
- 管理者ユーザー(Interfaceの多重継承も可能)
/
export interface AdminUser extends User, SoftDeletableEntity {
readonly permissions: readonly string[];
}
/
- スーパー管理者
/
export interface SuperAdminUser extends AdminUser {
masterKey: string;
}
この設計が圧倒的に優れている理由
1. コンパイルパフォーマンスの最適化:
Interfaceの `extends` は、シンボル解決の段階で構造がフラットにマージされるため、型チェッカーの負荷が劇的に軽減される。
2. 圧倒的な可読性:
IDEで `SuperAdminUser` にホバーした際、TypeScriptはこれを一つの洗練されたオブジェクト型として展開して表示してくれる。交差型のように `&` だらけのゴミ屋敷にならない。
3. 正確なエラーメッセージ:
プロパティの競合や型違反が起きた際、どのInterfaceのどのプロパティが原因かが一目でわかるメッセージが返ってくる。
4. 宣言的マージ(Declaration Merging)の恩恵:
Interfaceは同一スコープで同名定義すると自動マージされる特性がある(サードパーティライブラリの型拡張などで必須)。Type Aliasにはこれがない。
—
4. では、Type Alias(交差型)はいつ使うべきか?
ここまでInterfaceの優位性を説いてきたが、Type Aliasや交差型が「悪」というわけではない。適材適所である。
Type Aliasと交差型が真価を発揮するのは以下のようなケースだ。
- プリミティブ型の合成やユニオン型(`|`)を扱う場合(Interfaceはプリミティブを `extends` できない)
- 関数型のオーバーロードや合成
- Mapped Types や Conditional Types(条件付き型)の戻り値を受け止めるユーティリティ型を作る場合
// プリミティブやユニオンの絞り込みにはType Aliasが必須
type ID = string | number;
type Status = ‘active’ | ‘inactive’ | ‘suspended’;
// ユーティリティ型による合成(これらはType Aliasで行うべき)
type ResponseOf
data: T;
status: number;
error?: string;
};
オブジェクトの構造定義(特にドメインモデルやAPIスキーマ、コンポーネントのProps)は `interface`、それ以外の高度な型操作(演算・ユニオン・ユーティリティ)は `type` という明確な境界線をチームで引くことが、大規模開発におけるスケーラビリティを担保する。
—
5. チーフアーキテクトからの実践的提言
コードレビューで以下のアンチパターンを見つけたら、即座に修正を促してほしい。
1. 「とりあえず `&` で繋いでおけ」という思考停止:
オブジェクトの拡張に安易に交差型を使っている場合。今すぐ `interface … extends …` に書き換えさせること。
2. Propsの型定義で型エイリアスを乱用する:
ReactなどのコンポーネントPropsでも同様だ。`interface Props extends BaseProps` とすることで、親から子への型伝播のパフォーマンスが向上する。
TypeScriptは、書き手の意図をコンパイラに正確に伝えることで真価を発揮する言語だ。型システムの裏側のメカニズムを理解し、「なぜその構文を選ぶべきか」をロジカルに説明できるエンジニアこそが、プロダクトの寿命を延ばす真のアーキテクトである。