TypeScriptでinterfaceにできてtypeにできないことは?
解説
正解は同名の宣言による自動マージ(Declaration Merging)です。interface は同じ名前で複数回宣言すると自動的にマージされますが、type で同名を2回宣言するとコンパイルエラーになります。オブジェクト型の定義や関数シグネチャの定義はどちらでも可能で、ユニオン型の定義はむしろ type でしかできません。Declaration Mergingとは何かinterface の大きな特徴の一つが、同じ名前で複数回宣言すると自動的にプロパティが合成される仕組みです。interface User { name: string; } interface User { age: number; } // 結果的に { name: string; age: number } になる const user: User = { name: 'Taro', age: 25 };これはライブラリの型定義を外部から拡張するときに便利です。たとえば Express の Request に独自のプロパティを追加したり、Window オブジェクトにグローバル変数の型を足したりする場面で使われます。// express の Request を拡張する例 declare namespace Express { interface Request { userId: string; } }一方 type は同名で再宣言するとエラーです。type User = { name: string }; type User = { age: number }; // Error: Duplicate identifier 'User'typeにしかできないこと逆に type にしかできないこともあります。代表的なのはユニオン型・交差型・プリミティブのエイリアスです。// ユニオン型 type Status = 'success' | 'error' | 'loading'; // プリミティブのエイリアス type ID = string | number; // タプル型 type Pair = [string, number];これらは interface では表現できません。interface はあくまで「オブジェクトの形」を定義するためのもので、ユニオンやプリミティブは守備範囲外です。実務ではどちらを使うかチームや有名なプロジェクトでもスタンスが分かれるテーマですが、よくある方針は次のとおりです。オブジェクトの構造を定義するなら interface: extends による継承が読みやすく、Declaration Merging によるライブラリ拡張も効くユニオン型・タプル・プリミティブのエイリアスなら type: そもそも interface では書けない迷ったら type で統一するという派もある: type のほうが守備範囲が広く、後からユニオンに変えたくなっても書き直しが不要TypeScript公式ドキュメントでは「基本的に interface を使い、interface で表現できない型が必要なときに type を使う」と案内しています。ただしこれは絶対的なルールではなく、プロジェクトの方針に合わせるのが現実的です。extendsと交差型(&)の違い型の合成方法にも違いがあります。interface は extends、type は &(交差型)を使います。// interface の継承 interface Animal { name: string; } interface Dog extends Animal { breed: string; } // type の交差型 type Animal = { name: string }; type Dog = Animal & { breed: string };結果はほぼ同じですが、プロパティが衝突したときの挙動が異なります。interface extends は衝突するとコンパイルエラーで教えてくれますが、& はサイレントに never 型になることがあり、バグに気づきにくいケースがあります。型の継承が多い設計では interface のほうが安全です。