こんにちは。テックリードの私だ。
コードレビューのたびに「また `type` をなんとなく使ってしまったのか」「なぜここで `interface` ではなく `type` なのか説明できるか?」と問いただしたくなる場面に遭遇しない日はない。
世の入門書やネットの記事では「`interface` は拡張できる」「`type` はユニオンが書ける」といった、コンパイラの裏側を知らぬ者たちの表層的な違いばかりが語られている。だが、TypeScript 5.x時代を生きる我々は、型評価エンジンの挙動、IDEの補完パフォーマンス、そして宣言的拡張(Declaration Merging)の恩恵を深く理解した上で、道具を使い分けなければならない。
今回は、TypeScript 5.xにおける `interface` と `type` の最新トレンドと、プロダクション環境で破綻しないための「型設計の極意」をコードレビューの視点から伝授しよう。
—
1. 5.x時代の決定的なパラダイムシフト:「再帰的型エイリアス」と「パフォーマンス」
TypeScript 5.0以降、コンパイラの内部構造(特にJSDocの解析や型の効率化)は劇的な進化を遂げた。しかし、だからといって何でも `type` で書くことが正義になったわけではない。
ここで、実務の現場で頻出する「非同期API連携のレスポンス設計」と「コンポーネントのプロパティ設計」を例に、コンパイラがどう型を評価しているかを見ていこう。
悪臭を放つアンチパターン:思考停止の `type` 乱用
// 【アンチパターン】すべてを type で定義する
type UserID = string;
type UserProfile = {
id: UserID;
name: string;
metadata: Record
};
// これの何が問題か?
type AdminProfile = UserProfile & {
permissions: string[];
};
一見、何の問題もないように見える。しかし、この `&`(Intersection)を用いた型の合成は、TypeScriptの型チェッカーに対して重大な負荷をかける。
Intersection型は、コンパイル時に「すべてのプロパティの合流と競合解決」を遅延評価ではなく実体として計算しようとするため、巨大なコードベースにおいてはIDE(VSCodeなど)の型ヒント表示速度(Hovers)を著しく低下させる原因となる。
—
2. 実務で直面する課題:拡張性とパフォーマンスの両立
では、堅牢かつ高速なコンパイルを実現するためにはどうすればよいのか。
答えはシンプルだ。「オブジェクトの形状(Shape)の定義には `interface` を使い、変形・合成・プリミティブの制約には `type` を使う」という原点にして最良の原則に立ち返ることだ。
特に、TypeScript 5.xで強化された `satisfies` 演算子やテンプレートリテラル型と組み合わせる場合、基底となるデータ構造は `interface` で宣言的(Declarative)に書かれている方が、コンパイラのキャッシュ効率が良い。
プロダクション品質の設計パターン
以下のコードを見てほしい。非同期APIクライアントのレスポンスと、それを受け取るフロントエンドコンポーネントのプロパティ設計を、最も効率的な型アロケーションで構築した例だ。
/
- ==========================================
- Domain Model Definitions (Interface First)
- ==========================================
- オブジェクトの形状は interface で定義し、
- 宣言的拡張(Declaration Merging)の余地を残す。
/
export interface BaseEntity {
readonly id: string;
readonly createdAt: Readonly
readonly updatedAt: Readonly
}
export interface User extends BaseEntity {
name: string;
email: string;
role: UserRole; // 下部で定義された type を参照
}
/
- ==========================================
- Type Utilities & Primitives (Type Alias)
- ==========================================
- ユニオン型、マッピング型、プリミティブの制限には type を使用。
/
export type UserRole = ‘admin’ | ‘editor’ | ‘viewer’;
// 5.xのテンプレートリテラル型を活用した厳密なAPIエンドポイント型
export type ApiEndpoint = `/api/v1/${‘users’ | ‘posts’ | ‘comments’}/${string}`;
/
- オブジェクトのディープな部分更新を安全に行うためのユーティリティ型
- (typeならではの高度な条件付き型)
/
type DeepPartial
[K in keyof T]?: T[K] extends object ? DeepPartial
};
export type UserUpdatePayload = DeepPartial
/
- ==========================================
- Component Props Design
- ==========================================
/
export interface UserCardProps {
/ 表示するユーザーデータ。拡張性を考慮して interface を採用 /
readonly user: User;
/ ユーザー選択時のイベントハンドラー /
readonly onSelect: (userId: User[‘id’]) => void;
/ 5.xの satisfies と相性の良いオプションフラグ /
readonly variant?: ‘compact’ | ‘detailed’;
}
—
3. なぜこの設計が「堅牢」で「高速」なのか?
理由1: エラーメッセージの人間工学(DX)の向上
`interface` を用いて定義されたオブジェクト型は、TypeScriptのコンパイラがエラーメッセージを出力する際に、元のインターフェース名(例: `User`)をそのまま保持して表示してくれる傾向が強い。
一方、複雑な `type` と Intersection (`&`) の組み合わせは、エラー時にコンパイラが内部で展開した無機質なオブジェクト構造(`{ id: string; name: string; … } & { permissions: string[] }`)をそのまま吐き出し、開発者を絶望させることがある。
理由2: 宣言的拡張(Declaration Merging)のパワー
外部ライブラリの型や、グローバルな環境変数の型を拡張する際、`interface` ならば以下のように同名のインターフェースを定義するだけでマージされる。
// 既存のライブラリ型を拡張する例(ライブラリのバグや拡張漏れに対する最終防衛線)
declare module ‘some-ui-library’ {
interface ThemeConfig {
customProperty: string; // interface だからこそ安全にマージできる
}
}
`type` ではこれが不可能であり、一度別名で再定義して `Omit` や `Intersection` を挟む必要があるため、コードの複雑性が跳ね上がる。
—
4. チーフアーキテクトからの提言:今日からのコーディング規約
コードレビューで後輩やチームメンバーにこう指導してほしい。
1. データ構造・APIレスポンス・コンポーネントPropsはすべて `interface` で書く。
(将来的な拡張、IDEのホバー時の可読性、宣言的拡張のメリットを享受するため)
2. ユニオン型、プリミティブのエイリアス、タプル、Mapped Typesなどの「型の演算」が必要な場合のみ `type` を使う。
3. `type Foo = Bar & Baz` という安易なインターセクション結合を禁止し、可能な限り `interface Child extends Parent` を選択する。
TypeScriptの型システムは、単なる「エラーを防ぐための網」ではない。それは、ドキュメントであり、設計図であり、そして何よりコンパイル時のパフォーマンスを左右するエンジンだ。
道具の特性を正しく理解し、無駄な型計算を削ぎ落とした美しいコードベースを構築してほしい。君たちのプロダクトが、型エラーの恐怖から解放され、爆速でスケールすることを期待している。