こんにちは。プロダクトの規模が膨らみ、デザインシステムや共通UIコンポーネントの整備に本格的に取り組むフェーズにくると、誰もが一度はこの壁にぶぶつかります。
「ボタンやテキストのコンポーネントを作ったはいいものの、デザインの要望で『ここはやっぱり `` タグにしてリンクとして飛ばしたい』とか、『SEO的にここは `
` じゃなくて `
` にしたい』って言われた……。おいおい、また似たようなコンポーネントを量産するのか?」
……いや、待ってください。その度に `PrimaryButton` や `LinkButton` を乱立させるのは、未来の負債を前借りしているようなものです。
ここで登場するのが、MUIやRadix UIなどのモダンなライブラリでもこぞって使われている「Polymorphic Components(多態的コンポーネント)」です。
今回は、`as` 属性を使ってレンダリングされるHTMLタグを動的に変更しつつ、TypeScriptの型安全性を極限まで高めるテクニックについて、実務の現場でそのまま使えるレベルの知見を交えて徹底解説していきます。
—
1. Polymorphic Componentsとは何か?なぜ現場で必要なのか
Polymorphic Componentとは、ひとつのコンポーネントが、親から渡された `as` プロパティ(または類似の属性)に応じて、レンダリングするHTMLタグや別のReactコンポーネントを動的に切り替えられる仕組みのことです。
例えば、次のような要件を考えてみましょう。
{/ ボタンとして使いたい(
{/ 同じコンポーネントを、今度は画面遷移用のリンクとして使いたい(タグになってほしい) /}
ダッシュボードへ
外観やパディング、マージンなどの共通スタイルを保持したまま、裏側のDOM要素だけを自由に変えたい。これができれば、UIのコンポーネント設計は劇的にクリーンになります。
しかし、ここでTypeScript使いの我々の頭を悩ませる問題が発生します。
「`as=”button`」のときは `onClick` や `disabled` が補完されてほしいし、「`as=”a”`」のときは `href` や `target` が補完されてほしい。さらに、それぞれのHTMLタグが持つ固有の属性(`HTMLAttributes`)の型チェックも完全に効かせたい。
これを適当な `any` や甘い型定義で逃げていると、実務では必ずバグの温床になります。プロのエンジニアとして、型定義の妥協は許されない領域です。
—
2. ブラウザの裏側とReactの仕組み
私たちが書いた `as=”a”` というJSXは、最終的にReactのコアエンジンによってどのように処理されているでしょうか。
Reactは内部で、JSXのタグ名を受け取る際に「文字列(標準のHTMLタグ)」か「関数・オブジェクト(Reactコンポーネント)」かを判別しています。
JSXの仕様上、タグ名に大文字や変数、あるいはドット記法(例: `
// 概念的なイメージ
function Box({ as: Component = ‘div’, children, …rest }) {
// 小文字の文字列として渡された場合は標準DOM要素として生成される
return
}
ブラウザのレンダリングエンジン側から見れば、最終的に生成されるのは単なるDOMツリー(例: `…`)ですが、それを「開発者体験(DX)を落とさずに、完全に型安全な状態で構築する」のがTypeScriptの役割となります。
—
3. 実装:コピペで使える堅牢なPolymorphic Componentの型定義
それでは、実務で即座に使える、極めて堅牢なPolymorphic Componentの型定義と実装を見ていきましょう。
ここでは、あらゆるHTMLタグに変身できる万能な `
エディタにそのまま貼り付けて、ホバー時の型補完やエラーチェックを確認してみてください。
import React, {
ComponentPropsWithoutRef,
ElementType,
ReactNode
} from ‘react’;
/
- 1. 基本となるPropsの定義
- 独自のカスタムプロップス(例: 独自のマージンやバリアントなど)をここに定義します。
/
type BoxOwnProps
/ レンダリングしたいHTMLタグまたはReactコンポーネント /
as?: T;
/ 子要素 /
children?: ReactNode;
};
/
- 2. PolymorphicなPropsの結合型
- 「選んだタグの固有属性」と「自作のProps」をマージし、
- さらに `as` プロパティ自体の型を安全に制約します。
/
export type BoxProps
Omit
/
- 3. コンポーネントの実装
- ジェネリクスを受け取る関数コンポーネントとして定義します。
/
export const Box =
as,
children,
…rest
}: BoxProps
// デフォルトのタグを ‘div’ に設定
const Component = as || ‘div’;
return (
{children}
);
};
このコードの何がスゴいのか?(シニアからの解説)
1. `ElementType` によるタグの縛り
`T extends ElementType` とすることで、HTMLの標準タグ名(`’div’`, `’a’`, `’button’`, `’section’` 等)だけでなく、他のReactコンポーネントも `as` に指定できるようになります。
2. `ComponentPropsWithoutRef
例えば `as=”a”` と指定した瞬間、TypeScriptの型システムが働き、`rest` の型は `AnchorHTMLAttributes
3. `Omit` によるプロパティの衝突回避
もし自作のProps(例: `color` や `size` など)と、HTML標準の属性(例: `color` 属性など)で名前が衝突した場合、カスタムProps側の型が優先されるように `Omit` でうまく調停しています。
—
4. 実際のコンポーネントでの使用例
実際にこの `
import React from ‘react’;
import { Box } from ‘./Box’;
export const UserProfileCard = () => {
return (
{/ ケースA: 単なるラッパー(デフォルトの div タグになる) /}
{/ ケースB: 見出しとして使いたい(h2 タグにして、id属性を付与) /}
山田 太郎
{/ ケースC: リンクとして使いたい(a タグにして、hrefやtargetを安全に渡す) /}
公式サイトを見る
);
};
もしここで、`as=”button”` としているにもかかわらず `href=”/home”` なんぞという無謀なコードを書こうものなら、TypeScriptのコンパイラが即座に赤波線を引いて怒ってくれます。
「おい、`button` タグに `href` なんて属性はねぇよ!」と。この開発体験の高さが、チーム開発の品質を底上げする最大の武器になります。
—
5. 実務でハマりがちな「落とし穴」とベストプラクティス
最後に、現場でPolymorphic Componentsを運用する上で知っておくべき、いくつかの「実務の知見」をシェアしておきます。
落とし穴1: `forwardRef` との組み合わせ地獄
DOM要素への参照(`ref`)を取得したくなった途端、Polymorphic Componentsの型定義は一気に難易度が跳ね上がります。実は、標準の `React.forwardRef` はジェネリクスの伝搬と相性が悪く、そのまま書くと型が `any` に落ちたりコンパイルエラーの迷宮に迷い込みます。
対策:
自前で複雑な `forwardRef` のオーバーロードを書くのはメンテナンスコストが高すぎるため、もしRef転送がどうしても必要な場合は、無理に自作せず、Radix UI の `Slot` パターンや、定評のあるライブラリの設計思想を参考にすることをお勧めします。実務では「本当にそのコンポーネントでRefが必要か?」を一度立ち止まって考えてみてください。
落とし穴2: 肥大化するコンポーネント
「何にでも変身できるから」という理由で、あらゆるデザイン要素を1つの巨大なコンポーネントに詰め込もうとする人がたまにいます。結果として、プロパティが何十個も生えた「神コンポーネント(God Component)」が誕生し、誰も手を入်れられなくなります。
Polymorphicにするのは、「テキスト」「ボタン」「レイアウト用のBox」など、デザインシステムの基礎となるプリミティブなコンポーネントに留めるのが、長期的メンテナン性を保つ秘訣です。
—
まとめ
Polymorphic Componentsの型定義は、一見すると難解なTypeScriptのパズルのように見えるかもしれません。しかし、一度しっかりと仕組みを理解して共通基盤に組み込んでしまえば、「開発者のミスをコンパイル時に完全に検知し、柔軟性とDRY原則を両立させる」という、最強のフロントエンド武器になります。
ぜひ今日の業務から、あなたのプロジェクトの共通コンポーネントにこの知見を取り入れてみてください。コードの美しさと堅牢性が一段階引き上がるはずです。
それでは、また次回のアーキテクチャ談義でお会いしましょう。プロダクト開発、楽しんでいきましょう!

コメント