【結論】TypeScriptの型定義はtypeよりinterfaceを使うべき理由
はじめに
TypeScriptでコンポーネントのPropsやオブジェクトの型を定義するとき、typeとinterfaceのどちらを使うべきか、一度は悩んだことがあるのではないでしょうか。
巷では「どちらでも良い」「チームで統一されていればOK」といった意見もよく見かけます。
しかし、私は 明確な理由をもって「基本的にはinterfaceを使うべき」 だと主張します。
この記事では私の実体験で遭遇したReactのPropsの深刻なパフォーマンス問題を例に交えながら、なぜinterfaceが優れているのか、そしてtypeはどのような場面で使うべきなのかを解説します。
type aliasを使いたくなる魅力と、その裏に潜む罠
まず、なぜ多くの開発者がtypeを選びがちなのでしょうか。
それは、開発体験の良さにあります。
typeで定義した型は、VSCodeなどのエディタでホバーすると、最終的に解決された具体的な型情報がインラインで表示されます。
例として、ReactのPropsを定義してみましょう。
interfaceの型定義
export interface IconButtonProps extends HTMLAttributes<HTMLButtonElement> {
icon: React.ReactNode;
}
export const IconButton = (props: IconButtonProps) => {
// ...
};

interfaceの場合は名前しか表示されませんでした。
type aliasの型定義
export type IconButtonProps = HTMLAttributes<HTMLButtonElement> & {
icon: React.ReactNode;
};
export const IconButton = (props: IconButtonProps) => {
// ...
};

宣言した内容が表示されました。
このように、typeの場合、合成された型であっても、最終的な構造が一目でわかります。
私もこの「分かりやすさ」を理由に、業務では常にtypeを使うようにしていました。
しかし、これが後に大きな問題を引き起こすことになるのです。
type aliasが引き起こしたパフォーマンス地獄
私のチームが開発していたプロダクトが成長し、コードベースが大きくなってきたある日のことです。
なんの前触れもなく、エディタの型チェックが異常に遅くなりました。
文字を打つたびにCPUが唸り、一向にTypeScriptの型情報が表示されません。
さらに深刻だったのは、CI/CDでのビルド時間です。それまで1分程度で完了していたtscによる型チェックが、突然30分以上かかるようになったのです。
幸いなことに、この現象はまだマージ前の特定のブランチでのみ発生していたため、対象のブランチでの作業を一時停止し、他のブランチでの開発を続けることができました。
しかし、このブランチでの変更は重要な変更であり、早急にマージしたいものでした。
チームメンバー全員で1週間以上、設定を見直したり、ライブラリをダウングレードしたりと、あらゆることを試しました。しかし、原因は一向に分かりませんでした。
問題が発生したコミットは、共通フォームコンポーネントを使ったフォームフィールドの追加でした。
この共通フォームコンポーネントは、バリデーションライブラリのスキーマから型を推論する仕組みを持つ、比較的複雑な構造をしています。
しかし、さらに大規模かつ複雑な型システムを持つTanStackなどのライブラリでは同様の問題が起きていないことから、「型が複雑だから遅い」という単純な話ではないのではないかと考え始めました。
万策尽きた私は藁にもすがる思いで、ある仮説を試しました。「もしかして、typeが原因なのでは?」と。
試しにプロジェクト全体の型定義を、機械的にtypeからinterfaceに一括置換する正規表現を書き、実行してみました。
すると、どうでしょう。あれだけ私たちを苦しめていたエディタの遅延はなくなり、ビルド時間は1分に戻ったのです。
犯人は、私たちが便利だと信じて使っていたtypeでした。
もし同様の問題でお困りの場合
ここに、VSCodeの一括置換に使える正規表現を用意して置きました!
Ctrl + Shift + FまたはCmd + Shift + Fで検索パネルを開き、正規表現を有効にしたうえで以下の設定で検索・置換できます。
Find: ^(\s*)(?:export\s+|declare\s+)*type\s+([A-Za-z_$][\w$]*(?:<[^>]*>)?)\s*=\s*(.+?)\s*&\s*\{\s*\r?\n([\s\S]*?)^\1\}\s*;?
Replace: $1interface $2 extends $3 {
$4$1}
複数交差している場合はこちらを繰り返し適用してください。
Find: ^(\s*interface\b[^{]*\bextends\b[^{}]*?)\s*&\s*
Replace: $1,
なぜinterfaceはパフォーマンスに優れるのか?
理由は単純で、typeとinterfaceでは型の計算タイミングが異なるからです。
type:即時評価(Eager Evaluation)
typeは型エイリアスです。
つまり、既存の型に別の名前を付ける機能です。
TypeScriptコンパイラはtypeを見つけると、その場で型を再帰的に解決・展開し、具体的な1つの型にしてから先に進みます。
先の例でホバー時に具体的な型が見えていたのは、まさにこの「即時評価」が行われている証拠です。
交差型 (&) が幾重にも重なった複雑な型定義では、この計算コストが指数関数的に増大し、型チェック全体のパフォーマンスを著しく低下させるのです。
interface:遅延評価(Lazy Evaluation)
一方、interfaceは新しい名前付きのオブジェクト型を宣言するものです。コンパイラはinterfaceを1つの名前(シンボル)として扱います。
interfaceで定義された型は、実際にそのプロパティが参照されるなど、型情報が必要になるまで、内部構造の完全な計算を遅延させます。 これが遅延評価です。
interfaceにホバーしても名前しか表示されないのは、このためです。
この遅延評価の仕組みにより、interfaceはどんなに複雑に継承されても、プロジェクトがどれだけ巨大になっても、パフォーマンスへの影響を最小限に抑えることができます。大規模なOSSライブラリのコードを見ると、そのほとんどがinterfaceで型定義されているのは、このスケーラビリティが理由です。
interfaceのほうがパフォーマンスに優れることが多いという事実は、TypeScriptの公式ドキュメントにも明記されています。
- Using interfaces with extends can often be more performant for the compiler than type aliases with intersections
訳: interfaceを extends で拡張する方が、型エイリアスを intersection で組み合わせるよりも、コンパイラにとってはパフォーマンスの良いことが多いです。
typeはいつ使うべきか?
では、typeは全く使うべきでないのでしょうか? いいえ、そんなことはありません。
interfaceでは表現できない型を定義する場合にのみ、typeを使うべきです。
具体的には、以下のようなケースです。
-
Union型(合併型)を定義するとき
type Status = 'success' | 'error' | 'loading'; -
タプル型を定義するとき
type UserTuple = [name: string, age: number]; -
Mapped Typesなど、複雑な型操作をするとき
type ReadonlyUser = { readonly [K in keyof User]: User[K]; }; -
プリミティブ型に別名を付けるとき
type UserID = string;
オブジェクトの「形状」を定義する以外の、上記のようなケースでtypeは真価を発揮します。
おまけ OSSに学ぶ、interfaceにおける可読性を高める工夫
大規模なOSSライブラリでは、interfaceを使いながらも可読性を高めるために、いくつかの工夫がされていました。
JSDocコメントの活用

例えば、Next.jsの型定義では、interfaceに対してJSDocコメントを充実させることで、ホバー時に具体的な説明が表示されるようにしています。
また、interface名を具体的かつ説明的に命名することで、コードを読むだけでその役割が理解できるように工夫されています。
ユーティリティ型の活用

Honoの型定義では、ユーザーに公開する直前でSimplify<T>というユーティリティ型を使い、ホバー時に見やすくしています。
これは以下のように実装されている、継承に含まれる型を展開するための型ユーティリティです。
type Simplify<T> = { [K in keyof T]: T[K] } & {};
末端で一度だけの利用かつ、含まれるプロパティの数が少ない場合には有効です。
このように工夫することで、interfaceの利点を活かしつつ、開発体験の向上も図ることができます。
結論 基本はinterface、できないことだけtype
「どちらかに統一した方が良い」という主張を時々見かけますが、私はこれが間違いだと考えています。
typeとinterfaceはそもそも役割が違うものであり、統一するべきではありません。
私たちが従うべきルールはシンプルです。
オブジェクトの形状を定義する際は、まず
interfaceを使う。interfaceで表現できない型(Union型など)を定義する必要がある場合に限り、typeを使う。
この指針に従うことで、TypeScriptの強力な型システムの恩恵を受けつつ、アプリケーションのパフォーマンスとスケーラビリティを将来にわたって維持できるでしょう。
typeのホバー時の分かりやすさは魅力的ですが、それはプロジェクトを蝕むパフォーマンス問題と引き換えになる可能性があることを、ぜひ覚えておいてください。
この記事が、type vs interface論争に1つの結論をもたらし、あなたの開発体験をより良いものにする一助となれば幸いです。
Discussion
即時評価と遅延評価の解説がとてもためになりました。
ただこの場合、規模が小さければtype統一でもよいのかな?と感じました。
パフォーマンスの問題がなければ、冒頭の「分かりやすさ」というメリットはあるわけですし。
Typescriptのハンドブックにも「typeの機能が必要になるまではinterfaceを使った方が良い」とあるので、基本的にはinterfaceを使用し、typeでしか実現できない表現があれば局所的にtypeを使用する方針が良さそうですね。
Differences Between Type Aliases and Interfaces
本記事をきっかけにGeminiと深く議論しました。
TypeScriptという名前から type が主役かと思いきや、記事が指摘するパフォーマンス(extends)などの実態を見ると、静的言語における**『契約』の主役は interface** であり、type は(Union型やタプル型など)むしろ struct(データ構造)に近い役割を担っているのでは、という結論になりました。
この言語の『名前』と『設計思想』のねじれが、interface vs type 論争の根底にあるのかもしれませんね。非常に興味深い記事でした。