宣言的UIの設計思想:TypeScriptのInterfaceで「壊れないコンポーネント」を構築する極意
フロントエンド開発において、Propsの設計は単なる「型の定義」ではない。それはコンポーネントの仕様書であり、開発者に対する契約(Contract)そのものだ。
TypeScriptの`interface`を使いこなすことは、単に型安全性を確保するだけでなく、コンポーネントの責務を分離し、将来的な変更に対する耐性(レジリエンス)を高めることに直結する。今回は、現場で遭遇する「緩い設計」を排除し、堅牢なProps設計を極めるための思考法を授ける。
—
1. Type Aliasか、Interfaceか? —— 迷うこと自体がナンセンスだ
よく「TypeとInterface、どっちを使えばいい?」という議論がなされるが、結論から言おう。「拡張性が必要なオブジェクト定義」には迷わず `interface` を使うべきだ。
理由は明確だ。
- 宣言のマージ(Declaration Merging): ライブラリの型定義を拡張したり、条件付きでPropsを追加する際に破壊的な変更を避けられる。
- パフォーマンス: コンパイラ(`tsc`)は `interface` をキャッシュしやすく、複雑な型解決において `type` よりも最適化が効きやすい。
// 悪い例: 拡張性のないType Alias
type ButtonProps = { label: string; onClick: () => void };
// 良い例: 拡張性を考慮したInterface
export interface ButtonProps {
label: string;
onClick: () => void;
}
—
2. 「継承」によるDRYの再定義 —— むやみな継承は害悪である
DRY(Don’t Repeat Yourself)を盲信して、深い継承階層を作っていないか?
コンポーネントのProps設計において、継承は「意味的な包含関係」にある時のみ使うべきだ。
アンチパターン:Propsの過剰な継承
`HTMLAttributes` を全て継承すると、コンポーネントが何を受け付けているのかブラックボックス化し、不要なプロパティが内部に伝播する。
ベストプラクティス:Pickを用いた「必要なものだけ」の抽出
特定の属性だけをフィルタリングし、意図を明確にするのがプロの流儀だ。
/
- 堅牢なButtonコンポーネントのProps設計
- 必要なHTML属性のみをPickすることで、意図しないDOM属性の混入を防ぐ
/
export interface ButtonProps extends Pick<
React.ButtonHTMLAttributes
‘disabled’ | ‘type’ | ‘aria-label’
> {
label: string;
variant: ‘primary’ | ‘secondary’;
onClick?: () => void; // 非同期イベントを考慮し、オプショナルにする
}
—
3. オプショナルプロパティの「正解」は、nullを混ぜないこと
多くのエンジニアがやりがちなミスが、`value?: string | null` といった冗長な型定義だ。
TypeScriptの世界では、`undefined` は「値が存在しない」ことを示し、`null` は「値が空である」ことを示す。
原則:Propsには `?` を使い、`null` は排除せよ。
`undefined` であれば、`Default Parameters` や `??` 演算子でエレガントに処理できるからだ。
export interface UserAvatarProps {
username: string;
// undefinedであればデフォルト画像を表示させる設計
imageUrl?: string;
size?: ‘sm’ | ‘md’ | ‘lg’;
}
const UserAvatar = ({ username, imageUrl, size = ‘md’ }: UserAvatarProps) => {
// imageUrlがundefinedならデフォルトへフォールバック
const src = imageUrl ?? ‘/default-avatar.png’;
return ;
};
—
4. プロダクションコードにおける「合成」の極致
複雑なコンポーネントは、Interfaceを組み合わせて構築する。これにより、コンポーネントの責務が明確になり、テスタビリティが向上する。
// ベースとなる型
interface BaseProps {
className?: string;
testId?: string;
}
// データの型
interface UserProfile {
id: string;
name: string;
}
// 合成されたProps
export interface UserCardProps extends BaseProps {
user: UserProfile;
onDelete: (id: string) => Promise
}
/
- 実務で使えるコンポーネントの骨組み
/
export const UserCard: React.FC
user,
onDelete,
className,
testId
}) => {
const handleDelete = async () => {
try {
await onDelete(user.id);
} catch (e) {
console.error(‘Failed to delete’, e);
}
};
return (
);
};
—
結論:型はドキュメントであり、設計図である
優れたTypeScriptコードは、読み手に「どう使えばいいか」を語りかける。
- `interface` で明確なコントラクトを定義する。
- `Pick` を使って不要な継承を排除し、コンポーネントのインターフェースを絞り込む。
- `undefined` を活用し、`null` の曖昧さをコードから排除する。
これらを徹底するだけで、あなたのチームのコンポーネントはバグの温床から、極めて保守性の高い資産へと生まれ変わるはずだ。
次のコードレビューでは、`type` を `interface` に書き換えるところから始めてみてほしい。細部の積み重ねが、プロダクト全体の「質」を決定づけるのだから。