【TypeScript設計論】大規模モノレポにおける Interface vs Type の終焉:コンパイラ性能と拡張性を支配する型管理戦略
コードレビューをしていると、いまだに次のような議論に遭遇する。
「インターフェースと型エイリアス、どっちを使うべきですか?」
この問いに対する答えを「機能的優劣」だけで語る段階は、すでに終わっている。実務で数十万行規模のモノレポ、共有パッケージ群、そして複雑な非同期API連携を扱うフロントエンド/フルスタック開発において、この2つの選択は「TypeScriptコンパイラの型チェッカーの挙動」「IDEのインテリセンスの速度」「チーム全体の拡張性」に直結する死活問題だ。
今回は、テクニカルリードの視点から、大規模開発の現場で破綻しないインターフェース(`interface`)と型エイリアス(`type`)の明確な境界線と、コンパイル負荷を最小化する実践的な型定義のアーキテクチャを伝授する。
—
1. コンパイラの裏側:なぜ `interface` は速く、`type` は重いのか?
まず、言語の深層を理解しよう。TypeScriptコンパイラ(`tsc`)がコードを評価する際、`interface` と `type` では型評価(Type Evaluation)のメカニズムが根本的に異なる。
- `interface`(宣言的・キャッシュ可能):
同一スコープ内で同名の `interface` が定義されると、自動的に統合される(Declaration Merging)。コンパイラはオブジェクトの形状(Shape)をあらかじめキャッシュしやすく、incremental build(増分ビルド)において非常に効率的に処理される。
- `type`(計算的・遅延評価):
型エイリアスは、右辺にある型演算子(Mapped Types, Conditional Types, Template Literal Types など)をその都度評価・展開する。複雑な交差型(Intersection `&`)多用された `type` は、コンパイラに多大なメモリ消費とCPU負荷(型パズルの解決)を強いる。
結論としての使い分けの原則
- オブジェクトの構造定義には原則として `interface` を使う。(Declaration Mergingの恩恵を受け、IDEの型ヒントが美しく、キャッシュ効率が良い)
- ユニオン型(`|`)、プリミティブ、タプル、Mapped/Conditional Typesなどの「計算結果」には `type` を使う。
—
2. モノレポ・共有パッケージにおける設計アンチパターン
よくある事故の筆頭が、共有パッケージ(例: `@company/api-types`)の全面的な `type` への置き換え、あるいは安易な交差型(`&`)の乱用だ。
❌ 破綻するアンチパターン:巨大な Intersection 型の量産
// 悪い例: 共有パッケージですべてを type と & で繋ぎ合わせる
export type User = { id: string; name: string; };
export type UserPermissions = { roles: string[]; };
export type UserSettings = { theme: ‘dark’ | ‘light’; };
// 毎回の型解決時にコンパイラがプロパティの衝突やオプショナル性を再計算させられる
export type AdminUser = User & UserPermissions & UserSettings & {
adminLevel: number;
};
何が問題か?
この `AdminUser` を何百個ものコンポーネントやAPIフックで参照すると、TypeScriptの言語サービス(IDE)はホバーするたびにプロパティの合成を計算し直し、やがて `Type instantiation is excessively deep and possibly infinite.` という悪夢のエラーや、エディタの爆発的な重さを引き起こす。
—
3. 実践:プロダクションコードに耐える堅牢な型管理アーキテクチャ
では、モノレポ環境において「拡張性」「保守性」「コンパイル速度」をすべてクリアする設計はどうあるべきか。実際のレイヤード・アーキテクチャに沿ったコードで示そう。
階層1: プリミティブ・ブランド型・ユニオン(`type` の独壇場)
まず、ビジネスドメインの根幹となる値や、状態の選択肢は `type` で定義する。ここでは計算やユニオンが主役であるため `interface` は使えない。
// types/common.ts
export type Brand
// プリミティブの安全性を担保するブランド型
export type UserId = Brand
export type ISO8601Date = Brand
// ステータス等の有限の選択肢はユニオン型
export type AsyncStatus = ‘idle’ | ‘loading’ | ‘success’ | ‘failure’;
階層2: APIエンティティとドメインモデル(`interface` + 拡張)
バックエンドから取得するDTOや、フロントエンドのドメインモデルはすべて `interface` で定義する。ここで重要なのは、`&` による交差型ではなく、`extends` による明確な継承関係を用いることだ。
// types/entity.ts
import { UserId, ISO8601Date } from ‘./common’;
// 基本エンティティ
export interface BaseEntity {
readonly id: UserId;
readonly createdAt: ISO8601Date;
readonly updatedAt: ISO8601Date;
}
// ユーザーエンティティ(extends によりコンパイラキャッシュが効く)
export interface User extends BaseEntity {
name: string;
email: string;
avatarUrl: string | null;
}
// 拡張エンティティ(宣言的拡張)
export interface AdminUser extends User {
readonly adminLevel: number;
permissions: readonly string[];
}
階層3: コンポーネントPropsと非同期API連携の統合
フロントエンドのコンポーネント設計においては、HTML要素の拡張や、APIレスポンスの型安全なマッピングが求められる。ここでも `interface` をベースにしつつ、必要に応じてユーティリティ型(`Omit`, `Pick`)を適用する。
// components/UserProfileCard.tsx
import React from ‘react’;
import { User } from ‘@company/api-types’;
// Propsの定義には interface を使用(将来的な拡張・モジュール augmentation に備える)
export interface UserProfileCardProps extends React.HTMLAttributes
// 必要なプロパティだけを安全にピック、または omit する
user: Pick
onEdit?: (userId: User[‘id’]) => void;
isLoading?: boolean;
}
export const UserProfileCard: React.FC
user,
onEdit,
isLoading = false,
className,
…rest
}) => {
if (isLoading) {
return
;
}
return (
{user.name}
{user.email}
);
};
—
4. モノレポ環境における「Module Augmentation(モジュール拡張)」の強力なメリット
なぜエンティティ定義に `interface` を強く推奨するのか。その決定的な理由は Module Augmentation にある。
例えば、OSSのライブラリや共有モノレポパッケージで定義された型に対して、自社プロジェクト特有のプロパティを後から安全に「継ぎ足し」たい場合、`interface` であればそれが可能になる。
// 自社プロジェクト内の別ファイル (globals.d.ts など)
import ‘@company/api-types’;
// 宣言的マージにより、パッケージ側のコードを汚さずに型を拡張できる
declare module ‘@company/api-types’ {
interface User {
// 自社独自のカスタム属性を全Userオブジェクトに自動付与
internalDepartmentCode?: string;
}
}
`type` ではこのような後からのマージ(拡張)は不可能であり、型安全性を維持したままサードパーティ製や共有パッケージの型をハックすることができなくなる。この柔軟性こそが、大規模開発における最大の防衛策となる。
—
5. テクニカルリードからのまとめ
大規模チームにおける型定義の管理は、単なる「書き方の好み」ではなく、プロジェクトの寿命とパフォーマンスを左右するアーキテクチャ上の意思決定である。
1. オブジェクトの形状・ドメインモデル・Props は、パフォーマンス(コンパイラキャッシュ)と拡張性(Declaration Merging)に優れた `interface` をファーストチョイスにする。
2. ユニオン型、プリミティブ、マッピング型、ユーティリティの計算結果 は `type` を使う。
3. `&` (Intersection) による無理な型の合成を避け、`extends` を用いたクリーンな継承ツリーを構築する。
この原則をチーム全体の共通認識としてコードレビューに組み込むことで、あなたのモノレポはどれだけスケールしても、常に高速で、バグの入り込む余地のない堅牢な要塞であり続ける。