【テクニカル・上級編】Interfaceの継承 vs Typeの交差型(Intersection):パフォーマンスと可読性の比較 – TypeScript コア・型システムの基礎解析バイブル

インターフェースの継承 vs 交差型:型システムの本質とコンパイラ最適化の深淵

TypeScriptの型システムは、一見すると「動的言語に静的な安全性を後付けするための便利なガードレール」のように見える。しかし、その実体は、TypeScriptコンパイラ(`tsc`)の内部で動作する、純粋かつ極めて高度な漸進的型推論エンジン(Gradual Type Inference Engine)である。

大規模なコードベースにおいて、型の肥大化はビルドパイプラインのボトルネックとなり、言語サーバー(LSP)のメモリ消費量を爆発させ、開発者のイテレーション速度を殺す。特に、オブジェクト形状の合成手法として選択される `interface` の継承 (`extends`) と `type` の交差型 (`&` / Intersection) は、コンパイラの型評価アルゴリズムにおいて、まったく異なるコスト構造を持っている。

本稿では、この2つのアプローチがTypeScriptのコンパイラ内部、メモリ、そしてLSPのイベントループに与える影響を、低レイヤの知見から徹底的に解剖する。

—

1. コンパイラ内部における型表現のパラダイムシフト

まず、TypeScriptコンパイラがメモリ上でこれらをどう表現しているかを理解する必要がある。

Interfaceの継承:構造的サブタイピングの事前解決

`interface A extends B` は、コンパイラ内部(TypeChecker)において 「型構造のフラットなマージ(扁平化)」 を引き起こす。
`A` は単に `B` のプロパティテーブルを継承し、自身のプロパティテーブルと統合された単一のシンボルとしてキャッシュされる。

interface Base {
id: string;
timestamp: number;
}

// コンパイラはこれを単一の結合されたオブジェクト型として内部キャッシュする
interface Extended extends Base {
payload: Record;
}

このアプローチの美しさは、型の評価(Type Evaluation)の遅延が最小限に抑えられる点にある。コンパイラが `Extended` 型を評価する際、すでにマージ済みのテーブルを参照するだけでよいため、型関係の解決(Subtyping check)が高速に行われる。

交差型(Intersection):遅延評価される論理演算

対して、`type A = B & C` は、構造の結合ではなく 「型の積集合(Intersection)を表す遅延評価ノード」 を生成する。

type BaseT = {
id: string;
timestamp: number;
};

type ExtendedT = BaseT & {
payload: Record;
};

ここで `ExtendedT` は、`BaseT` と右側のオブジェクト型を「合成しなさい」という指示書(AST Node / Type Node)として保持される。
これがパフォーマンスに牙をむくのは、ホバー時、エラーメッセージ生成時、あるいはさらに別の型と交差させた瞬間だ。コンパイラは、その場でプロパティの衝突解決(例:同名プロパティの型がプリミティブのユニオンになるか、neverになるかの判定)を計算し直さなければならない。

—

2. 大規模プロジェクトにおけるコンパイル時間とLSPの挙動

何千ものファイルと数十万行の型定義を持つモノリスにおいて、この違いは致命的な差を生む。

メモリ消費量とガベージコレクション(GC)

交差型を多用すると、TypeScriptの型チェッカー(`TypeChecker`)内部で無数の一時的な型インスタンス(Instantiation)が生成される。
V8エンジンのヒープメモリ上において、これらの複雑な交差型ツリーは参照の網の目を形成し、世代別GC(Generational GC)の老朽世代(Old Space)を圧迫する。特にLSP(tsserver)は、ユーザーがコードを1文字タイピングするたびに、インクリメンタルな型チェックのキューをメインスレッドのイベントループに積む。

交差型がネストしていると、エディタが補完候補(IntelliSense)を表示する際のリフレクションコストが指数関数的に増大する。

// 悪夢のネストした交差型(最悪のケース)
type DeepMerged = TypeA & TypeB & TypeC & TypeD & TypeE;
type RecursiveDeep = T & { parent: RecursiveDeep };

このような定義が散見されるコードベースでは、`tsserver.log` には頻繁なGCの停止(Stop-the-world)と、型チェックのタイムアウトが記録されることになる。

—

3. 実証的比較:Interface継承 vs 交差型

では、型安全性、可読性、そしてパフォーマンスの観点でどのように使い分けるべきか。以下の実用的なコードで検証する。

パターンA:Interfaceによる堅牢な拡張(推奨)

APIレスポンスやドメインモデルなど、明確な階層構造を持つ場合は `interface` を使うべきである。

// ==========================================
// パターンA: Interfaceの継承
// ==========================================

export interface UserEntity {
readonly id: string;
email: string;
version: number;
}

export interface UserProfile extends UserEntity {
firstName: string;
lastName: string;
avatarUrl: string;
}

export interface AdminUser extends UserProfile {
permissions: ReadonlyArray;
securityClearanceLevel: number;
}

// 実行時のコストはゼロであり、コンパイル時の型解決も極めて高速
const admin: AdminUser = {
id: “usr_01H…”,
email: “architect@core.internal”,
version: 1,
firstName: “Naohiro”,
lastName: “Chief”,
avatarUrl: “https://…”,
permissions: [“ROOT”],
securityClearanceLevel: 5
};

パターンB:交差型が必要になる唯一の正当領域(ユーティリティとブランド型)

では、交差型はどこで使うべきか? それは 「既存の型を破壊せずに追加の振る舞いを合成する場合(Mixins)」 や 「プリミティブをカプセル化するブランド型(Branded Types)」 だ。

// ==========================================
// パターンB: 交差型の真価(ブランド型とMixin)
// ==========================================

// 1. ブランド型による型の安全なプリミティブ剥奪
type Brand = T & { readonly __brand: U };

export type UserId = Brand;
export type OrderId = Brand;

function processOrder(userId: UserId, orderId: OrderId) {
// string同士だが、型レベルで混同を完全に防ぐ(実行時オーバーヘッドはゼロ)
}

// 2. Mixinパターンの型付け
type Timestamped = T & { createdAt: number; updated: number; };
type SoftDeletable = T & { deletedAt: number | null; };

function withAudit(target: T): Timestamped> {
return {
…target,
createdAt: Date.now(),
updated: Date.now(),
deletedAt: null,
};
}

—

4. チーフアーキテクトからの提言:型設計の黄金律

1. 基本は `interface` で書く
オブジェクトの形状を定義し、拡張していくドメインモデルにおいては、常に `interface` と `extends` をファーストチョイスにせよ。これによりLSPのキャッシュ効率が最大化され、エディタの補完がミリ秒単位で応答するようになる。
2. 交差型 (`&`) は「演算子」として扱う
交差型は、ジェネリクス制約、ブランド型、あるいはユーティリティ関数による型の合成(Mixin)といった、メタプログラミング的な文脈に限定して使用する。無闇にオブジェクトの合成に `&` を使うべきではない。
3. 同名プロパティの衝突に警戒せよ
`interface` の継承で同名プロパティを異なる型で上書きしようとすると、コンパイラは即座にエラーを吐く(安全性の担保)。一方、交差型で同名プロパティを衝突させると、そのプロパティは自動的に `never`(または互換性のあるユニオン)に評価され、エラーメッセージが極めて難解になる。

TypeScriptの型システムをマスターするということは、単にコードを綺麗に書くことではない。コンパイラの脳内(Type Checkerのアルゴリズム)を完全にハックし、その計算量を最小限に抑える構造をコードとして表現することである。

この知見を胸に、あなたのプロジェクトの型定義を今すぐ見直してほしい。ビルド時間の短縮と、開発体験の劇的な改善がそこにあるはずだ。

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