境界線上のアーキテクチャ:`interface` と `type` の深層と、本番環境で迷わないための設計指針
TypeScriptを日常の武器として使いこなすようになると、必ず直面する究極の二者択一がある。
「ここは `interface` を使うべきか、それとも `type` を選ぶべきか?」
初学者のうちは「オブジェクトなら `interface`、それ以外なら `type`」という教科書通りのルールで事足りるかもしれない。しかし、数万行規模のコードベースを支え、V8エンジンのメモリ効率やコンパイル速度、さらにはチーム開発におけるスケーラビリティまでを考慮する上級エンジニアにとって、この選択は単なるコーディングスタイルの問題ではない。それはアプリケーションの堅牢性を左右するアーキテクチャ上の重大な意思決定なのだ。
今回は、V8の型チェックの内部挙動やコンパイラの最適化、そして実務の現場で泥臭く培ってきた知見を交えながら、両者の本質的な違いと、迷いなくコードベースを統率するための設計基準を紐解いていこう。
—
1. 内部挙動とパフォーマンスの現実:コンパイラは何を見ているのか
まず、TypeScriptのコンパイラ(tsc)がこれらをどのように処理しているのか、その裏側の話をしよう。
よく「`interface` の方がパフォーマンスが良い」という都市伝説が囁かれることがある。これは半分正しく、半分は誤りだ。
`interface` は、TypeScriptの型チェッカー内部において、常に単一のオブジェクト型として名前付きでキャッシュされやすい。一方、複雑な交差型(Intersection Types: `A & B`)を `type` で多用すると、コンパイラはプロパティの評価を遅延評価(Lazy Evaluation)させたり、型の解決時に膨大なメモリを消費したりする。
特に、巨大なサードパーティ製ライブラリの型定義を `&` でガンガン結合していくと、TypeScriptの言語サーバー(tsserver)が重くなり、VS Codeのエディタ上で補完が数秒遅れるという「開発者体験の崩壊」を引き起こす。この現象に直面した読者も少なくないはずだ。
宣言の結合(Declaration Merging):両者の決定的な分岐点
機能的な最大の違いであり、アーキテクチャに最も大きな影響を与えるのが 宣言の結合(Declaration Merging) の有無だ。
`interface` は、同じスコープ内で同名のものを複数定義すると、自動的にプロパティがマージされる。
// フレームワークやライブラリの拡張でよく使われる手法
interface Window {
originalTheme: string;
}
// 別ファイルやモジュール augmentation でも同じ interface を拡張できる
interface Window {
analyticsEnabled: boolean;
}
// コンパイル後、Window は両方のプロパティを持つ一つの型として統合される
const checkWindow = (win: Window) => {
console.log(win.originalTheme);
console.log(win.analyticsEnabled);
};
この「後から拡張できる」という性質は、既存のグローバルオブジェクトやライブラリの型定義を拡張(Module Augmentation)する際には神の機能となる。しかし、クローズドなビジネスロジックのドメインモデルにおいては、意図しないプロパティの混入を許す脆弱性にもなり得る。
対して、`type`(型エイリアス)は宣言の結合を一切許さない。同じ名前で定義しようものなら、コンパイラは容赦なく「Identifier Duplicated」エラーを吐き出す。
// type で同じことをやろうとするとエラーになる
type UserState = {
id: string;
};
// ❌ エラー: Duplicate identifier ‘UserState’.
type UserState = {
permissions: string[];
};
この「厳格なイミュータビリティ(不変性)」こそが、`type` が堅牢なアプリケーション設計において好まれる理由の一つである。
—
2. 表現力の限界:なぜ複雑な型操作には `type` が不可欠なのか
`interface` はオブジェクトの形を定義することには特化しているが、プリミティブ型、ユニオン型(Union)、タプル型、そしてMapped Typesなどの高度な型演算を扱うことはできない。
// 1. プリミティブの別名
type ID = string | number;
// 2. タプル型(厳密な順序を持つ配列)
type Point2D = [number, number];
// 3. ユニオン型とリテラル型を組み合わせた状態管理
type NetworkState =
| { status: ‘IDLE’ }
| { status: ‘LOADING’; progress: number }
| { status: ‘SUCCESS’; data: unknown }
| { status: ‘FAILURE’; error: Error };
これらはすべて `type` の独壇場だ。特に、ReduxやZustandといった状態管理ライブラリ、あるいはAPIクライアントのレスポンス型定義において、ユニオン型による「不正な状態を表現不可能な型にする(Making impossible states impossible)」という設計手法は、フロントエンドのバグを根絶するための強力な武器となる。
では、`interface` が拡張(`extends`)できるのに対して、`type` は交差型(`&`)で同様のことができるのではないか? ここに大きな罠がある。
// interface の拡張
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
// type の交差型
type AnimalType = {
name: string;
};
type DogType = AnimalType & {
bark(): void;
};
一見すると同じように見えるが、プロパティの型が競合したときの振る舞いが決定的に異なる。
`interface` で同じプロパティを異なる型で拡張しようとすると、コンパイル時に明確なエラーとして検知される。しかし、`type` の交差型では、競合したプロパティが `never` 型になり、エラーに気づきにくいまま実行時バグの温床になることがある。
—
3. 実務における設計基準:チームで迷わないための指針
現場のコードレビューにおいて、「ここは `type` にすべきか `interface` にすべきか」という不毛な議論で時間を浪費するのは避けたい。
チーフアーキテクトとして、チームに徹底すべき明確な選定基準を提示しよう。
原則 1: デフォルトは `type` を採用する
モダンなTypeScript開発においては、オブジェクト、プリミティブ、ユニオン、関数型など、あらゆる型定義を `type` で統一するのが最もメンテナブルである。
理由:
- 意図しない宣言の結合を防ぎ、予期せぬバグの混入を防げる。
- チームメンバーが「これは `interface` にすべきか?」と悩む認知負荷をゼロにできる。
- Mapped TypesやConditional Typesなどの高度な型ユーティリティへスムーズに移行できる。
原則 2: `interface` は「拡張性」が明確に必要な場合のみ限定的に使う
では、`interface` はいつ使うべきか?
- ライブラリの型定義やプラグイン機構を作っており、外部からの `extends` や `Module Augmentation` を意図的に許可する場合。
- OOP(オブジェクト指向プログラミング)的なクラスの `implements` 対象として、明確な契約(Contract)を定義する場合。
これ以外のユースケース、例えば単なるコンポーネントのPropsやAPIのレスポンス定義であれば、`type` で完全に代替可能であり、むしろその方が安全である。
—
4. 実用的なコード例:堅牢なコンポーネント設計の比較
最後に、実際のReactコンポーネントのProps設計を例に、両者のアプローチの違いを確認しよう。
// ==========================================
// パターンA: type による安全で柔軟な定義(推奨)
// ==========================================
type ButtonBaseProps = {
onClick: () => void;
disabled?: boolean;
};
type PrimaryButtonProps = ButtonBaseProps & {
variant: ‘primary’;
accentColor: string;
};
type SecondaryButtonProps = ButtonBaseProps & {
variant: ‘secondary’;
borderWidth: number;
};
// ユニオン型として完全に状態を排他制御する
type ButtonProps = PrimaryButtonProps | SecondaryButtonProps;
export const RobustButton = (props: ButtonProps) => {
if (props.variant === ‘primary’) {
// このブロック内では props は PrimaryButtonProps として型ガードされる
return ;
}
return ;
};
この設計では、`ButtonProps` が判別可能ユニオン(Discriminated Union)として機能しており、不適切なプロパティの組み合わせ(例:`variant` が `primary` なのに `borderWidth` を渡すなど)をコンパイルエラーとして完全にシャットアウトしている。
—
結びに代えて
TypeScriptにおける `interface` と `type` の選択は、単なるシンタックスの好みではない。それは、アプリケーションの型安全性の境界線を引き、コンパイラの最適化を引き出し、将来の拡張性と保守性を担保するための重要なアーキテクチャ設計そのものである。
「なぜこの型は `type` なのか」「なぜここはあえて `interface` で宣言の結合を使っているのか」。
コードを書くすべての瞬間にその意図を持たせること。それこそが、ただコードを動かすエンジニアから、システム全体を深く統率する真のスペシャリストへの境界線なのである。

コメント