【実務・中級編】TypeScriptのInterfaceで実現する「宣言的UIコンポーネント」のProps設計 – TypeScript コア・型システムの基礎解析バイブル

宣言的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 {username};
};

—

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 (

{user.name}

);
};

—

結論:型はドキュメントであり、設計図である

優れたTypeScriptコードは、読み手に「どう使えばいいか」を語りかける。

  • `interface` で明確なコントラクトを定義する。
  • `Pick` を使って不要な継承を排除し、コンポーネントのインターフェースを絞り込む。
  • `undefined` を活用し、`null` の曖昧さをコードから排除する。

これらを徹底するだけで、あなたのチームのコンポーネントはバグの温床から、極めて保守性の高い資産へと生まれ変わるはずだ。
次のコードレビューでは、`type` を `interface` に書き換えるところから始めてみてほしい。細部の積み重ねが、プロダクト全体の「質」を決定づけるのだから。

タイトルとURLをコピーしました