こんにちは。チームのコードレビューをしていて、「あ、またこのボロが出てるな」と思う瞬間がある。それは、既存のHTML要素をラップした汎用コンポーネントを作るとき、Propsの型定義で車輪の再発明をしていたり、`any`や`unknown`で濁していたりするコードを見たときだ。
例えば、ただのスタイル付きボタンや、ちょっとしたラッパーdivを作るだけのコンポーネントなのに、`onClick`や`className`、果てはアクセシビリティ系の属性(`aria-`)まで律儀に自分で型定義し直しているコードを見かける。
君ね、ブラウザが裏側でどれだけの標準属性をサポートしていると思っているんだ?それを手動でメンテするなど、仕事量を増やすだけの修行でしかない。
今回は、TypeScriptの Intersection Types(交差型: `&`) を使って、既存のHTML属性と俺たちのカスタムPropsをエレガントに、かつ安全に合成する実務の極意を授けよう。中級から一歩抜け出して、チームから「おっ、こいつの書くコードは違うな」と思われるための必須スキルだ。
—
なぜIntersection Types(`&`)が必要なのか?
実務でフロントエンドを書いていれば、「ネイティブの`
ここで、素朴なエンジニアはこう書きがちだ。
// ❌ やってはいけない残念な例
type BadButtonProps = {
text: string;
variant: ‘primary’ | ‘secondary’;
onClick: () => void; // ← えっ、じゃあ `onMouseEnter` とか `form` 属性は?使えなくなるの?
};
これだと、いざ使う側が「あ、ここにホバー時のイベント入れたいな」とか「`type=”submit”`を指定したいな」と思ったときに、型エラーで弾かれてしまう。かといって、ネイティブの型を全部手打ちで定義し直すのは、TypeScriptの恩恵をドブに捨てるようなものだ。
ここで登場するのが、TypeScriptの `&`(アンパサンド)演算子による Intersection Types だ。これを使えば、「既存のHTML要素の型」と「俺たちのカスタム型」を合体させ、1つの完璧なProps型を作り上げることができる。
—
現場で即戦力になる「合成ボタンコンポーネント」の実装
百聞は一見にしかず。実務の現場でそのままコピペして明日から使える、堅牢なボタンコンポーネントのコードを見てほしい。Reactの組み込み型である `ComponentPropsWithoutRef` や `ComponentProps` を組み合わせて使うのが、現代のReact + TypeScriptにおけるベストプラクティスだ。
import React, { ComponentPropsWithoutRef } from ‘react’;
// 1. このコンポーネント独自のカスタムPropsを定義する
type CustomButtonProps = {
/ ボタンの見た目のバリエーション /
variant?: ‘primary’ | ‘secondary’ | ‘danger’;
/ ローディング中かどうか。trueならスピナーを出してクリックを無効化する /
isLoading?: boolean;
/ 子要素としてテキストやアイコンを受け取る /
children: React.ReactNode;
};
// 2. ネイティブの
export const Button: React.FC
variant = ‘primary’,
isLoading = false,
children,
disabled,
className = ”,
…rest //残りの属性(onClick, type, aria- など)を全てスプレッドするために回収
}) => {
// スタイルやクラス名の切り分けロジック(簡略化しています)
const baseStyles = “px-4 py-2 rounded font-medium transition-colors focus:outline-none”;
const variantStyles = {
primary: “bg-blue-600 text-white hover:bg-blue-700”,
secondary: “bg-gray-200 text-gray-800 hover:bg-gray-300”,
danger: “bg-red-600 text-white hover:bg-red-700”,
}[variant];
return (
);
};
このコードの何が優れているのか?
1. 完全な型補完と安全性:
使う側は、`` のように、標準の`
—
実務でハマりがちな「型競合(Type Clash)」の罠と回避策
さて、ここからがシニアの経験談だ。Intersection Typesを使って型を合成するとき、時々意地悪なエラーに直面する。それが 「プロパティの型競合」 だ。
例えば、HTMLの標準属性と、自分が定義したいカスタムPropsで「同じ名前のプロパティ」が存在する場合、TypeScriptはコンフリクトを起こして `never` 型になってしまうことがある。
type BadExampleProps = ComponentPropsWithoutRef<'div'> & {
// もし万が一、HTMLのdivが持っているプロパティと被る名前を定義してしまったら…
style: string; // HTML側は CSSProperties (オブジェクト) なので型が衝突する!
};
こういうときは、Intersection Typesの特性を利用して、後から結合する型で既存の型を上書き(オーバーライド) すればいい。
type SafeProps = Omit
// Omitで標準の厄介な型をいったん除外し、自分の好きな型で安全に定義し直す
style?: React.CSSProperties | string;
};
`Omit` と Intersection Types(`&`)を組み合わせるテクニックは、サードパーティ製のUIライブラリをラップする際や、複雑な独自コンポーネント設計では避けて通れない必須教養だ。これをマスターしておくと、どんな厄介な型定義の要求が来ても涼しい顔で対応できるようになる。
—
チーフアーキテクトからのメッセージ
Propsの型定義は、単なるTypeScriptの「お作法」やエラー逃れではない。それは、「このコンポーネントはこういう文脈で使われてほしい」「こういう責務を持っている」という設計思想そのものをコードに定着させる強力なドキュメントだ。
Intersection Typesを用いたプロパティの合成を使いこなせるようになると、コンポーネントの「再利用性」と「型安全性」のバランスが劇的に向上する。無駄なコードを書く必要が消え、目の前のビジネスロジックやUIのクオリティに集中できるようになるはずだ。
さあ、明日からのコードレビューでは、「手動で全属性を定義しているダサいコード」を見つけたら、すかさず今回のテクニックを教えてやってくれ。チーム全体のレベルを引き上げるのは、いつだって君のような中堅エンジニアの気概なのだから。

コメント