こんにちは。チームのコードレビューをしていて、一番「あぁ、またか」とため息が出る瞬間ってどんな時ですか?
私はね、PR(プルリクエスト)の差分を見たときに、あるコンポーネントのProps定義が `type` で書かれていたり、別のファイルでは `interface` で書かれていたり、プロジェクト内でルールがごちゃ混ぜになっているのを見つけた時です。
「動けばどっちでも同じじゃん」って?
いやいや、甘い。中級からシニアへ駆け上がるこのフェーズにおいて、その「どっちでもいいや」という妥協が、のちの巨大な負債や、TypeScriptの型推論エンジンを無駄に苦しめる原因になるんです。
今日は、ReactコンポーネントのProps定義における `interface` と `type` の使い分けについて、実務の現場で即座に使える決定版の基準を授けましょう。ブラウザやTypeScriptの裏側の挙動まで踏み込んで、スッキリ腹落ちさせてあげますよ。
—
なぜこの論争が起きるのか?(TypeScriptの裏側の話)
まず大前提として知っておいてほしいのは、TypeScriptの `interface` も `type` も、コンパイルされてブラウザに届く頃には跡形もなく消え去っているということです。ブラウザが理解するのは、ただのJavaScriptオブジェクトだけですからね。
じゃあ何が違うのか? それはTypeScriptのコンパイラ(型チェッカー)が、エディタ上やビルド時にどう型を解釈し、どうエラーメッセージを吐くかの「処理の仕組み」にあります。
特にReactのコンポーネント開発において、この2者の最大の違いは 「拡張性(Extensibility)」 と 「エラーメッセージの親切さ」 に現れます。
ざっくり結論から言いましょう。私のチームでは、コンポーネントのPropsは原則として `interface` を使い、合成型やプリミティブの別名定義などが必要な特殊なケースでのみ `type` を使う というルールを敷いています。その理由を、現場のリアルなコードと共に見ていきましょう。
—
現場で即採用できる!推奨される使い分け基準
1. 原則:コンポーネントのPropsは `interface` を使う
Reactのコンポーネントを書くとき、私たちは必ずと言っていいほど「拡張(Inheritance / Merging)」を行います。例えば、既存のHTML要素(`
ここで `interface` の真価が発揮されます。`interface` は同じ名前で宣言すると自動的に結合される「宣言的マージ(Declaration Merging)」の性質を持っていますが、それ以上に `extends` キーワードを使ったオブジェクト指向的な拡張が非常に直感的です。
実際に、現場でよくある「ローディング機能付きカスタムボタン」のコードを見てみましょう。
import React, { ComponentPropsWithoutRef } from ‘react’;
// 1. ベースとなるHTMLのbutton要素が持つすべての属性を型として取得
type NativeButtonProps = ComponentPropsWithoutRef<'button'>;
// 2. コンポーネント独自のPropsを定義
type CustomButtonSpecificProps = {
isLoading?: boolean;
variant?: ‘primary’ | ‘secondary’ | ‘danger’;
};
// 【推奨】コンポーネントのProps定義には interface を採用する
// メリット: エラーメッセージが圧倒的に読みやすく、IDEのホバー時の見栄えが良い
export interface ButtonProps extends NativeButtonProps, CustomButtonSpecificProps {
// 必要であればここでさらに上書き定義も可能
onClick?: (event: React.MouseEvent
}
export const Button: React.FC
isLoading = false,
variant = ‘primary’,
children,
disabled,
…rest // 残りのネイティブな属性(aria属性やdata属性など)をスプレッド
}) => {
return (
);
};
なぜ `interface` が優れているのか?
TypeScriptが型エラーを検知したとき、`interface` で組まれた型は、エディタ(VSCodeなど)上で非常にきれいなオブジェクト構造としてホバー表示されます。また、コンパイラキャッシュの効率面でも、オブジェクトの形状(Shape)をキャッシュしやすい `interface` の方が、大規模アプリケーションにおいて型チェックのパフォーマンスが有利になりやすいという実務上のメリットもあります。
—
2. 例外:`type` を使わざるを得ない、あるいは使うべきケース
じゃあ `type` は不要なのか? 全くそんなことはありません。`type`(エイリアス)は、オブジェクトの「設計図」を作るためではなく、「型の名前付けや合成(Union)」 のために存在しています。
次のようなケースでは、迷わず `type` を使いましょう。
- ユニオン型(Union Types)や交差型(Intersection Types)を定義する場合
- プリミティブ型、タプル型、リテラル型に別名をつけたい場合
具体的なコードで確認します。
import React from ‘react’;
// ケースA: 状態やサイズの選択肢をユニオン型で定義する場合(type一択)
export type ButtonSize = ‘sm’ | ‘md’ | ‘lg’;
export type ButtonStatus = ‘idle’ | ‘loading’ | ‘success’ | ‘error’;
// ケースB: 複数の型を複雑に組み合わせる必要がある場合
type IconProps = {
name: string;
};
type ImageProps = {
src: string;
};
// 排他的なProps(アイコンか画像のどちらか一方しか受け付けない設計)をUnionで表現
// ※このような複雑な合成は interface の extends では表現しづらい
export type AvatarProps = (IconProps | ImageProps) & {
size?: ButtonSize;
altText: string;
};
export const Avatar: React.FC
// 実装は省略しますが、ここで安全に型ガードを効かせることができます
return
;
};
もしあなたが「オブジェクトの形を定義する」のであれば `interface` を使い、「型の組み合わせや別名定義」をするのであれば `type` を使う。この棲み分けが頭に染みついていれば、もう迷うことはありません。
—
シニアが教える「やってはいけないアンチパターン」
現場でたまに見かける、絶対にやめてほしい書き方を共有しておきます。
1. すべてのファイルを `type` で統一するという極端なルール
「ウチのチームは一貫性を持たせるために全部 `type` にしています」という現場がありますが、これはTypeScriptの恩恵を半分捨てているようなものです。拡張性が必要なコンポーネントPropsで `type & type` の乱用(Intersectionの多用)をすると、IDEのエラーメッセージが「何が何だか分からない長文」になり、デバッグ効率が著しく落ちます。
2. ファイルごとに気分で変える
Aのコンポーネントは `interface`、隣のBのコンポーネントは `type`。特に理由もなく混在していると、コードのレビュー時に「これ何か意図があるの?」という無駄なcognitive load(認知負荷)をチームメンバーに強いることになります。
—
まとめ:今日の振り返り
- コンポーネントのProps定義は、拡張性が高くエラー表示もスマートな `interface` をデフォルト(標準)に据えよう。
- Union型(`A | B`)や複雑な型合成、プリミティブの別名定義には、その真価を発揮する `type` を使おう。
型定義というのは、ただTypeScriptの赤線を消すための儀式ではありません。「未来の自分や、チームの仲間に対する最高のドキュメント」 です。
明日からのコードレビューで、メンバーの書いたProps定義の `interface` と `type` が正しく使い分けられているか、ぜひチェックしてみてください。「お、分かってるね」と一目置かれるシニアエンジニアになれるはずですよ。それでは、また!

コメント