こんにちは。君もそろそろ「単に動くだけのReactコード」から卒業し、大規模なアプリケーションを破綻させずにスケールさせるための設計力に直面するころだな。
実務でコードベースが肥大化してくると、一番最初に頭を悩ませるのが「コンポーネント間のPropsの重複と型定義のメンテ地獄」だ。特に、既存のUIコンポーネント(例えば、弊社の誇る強固な`Button`)のPropsを少しだけ改変して、新しい別のコンポーネントを作りたい時、君はどうしている? まさか、イチから型を書き直したり、コピペして手動で一部を書き換えたりしてはいないだろうね?
今回は、TypeScriptのユーティリティ型である `Pick` と `Omit` を駆使して、Propsのフィルタリングをスマートに行い、DRY(Don’t Repeat Yourself)原則を極限まで守り抜くための実践的なテクニックを授けよう。
—
なぜPropsの「流用とフィルタリング」が必要なのか?
実務の現場を思い出してほしい。よくある要件として、「基本のボタンコンポーネントはあるけれど、特定の画面専用のスペシャルなボタンを作りたい。ただし、その画面ではクリック時のイベント(`onClick`)の挙動を完全にラップしたいから、外部からの直接の `onClick` は受け付けたくないんだよな……」といったシチュエーションがある。
ここで、ベースとなる `ButtonProps` をそのまま流用しつつ、不要なプロパティを削ぎ落としたり(`Omit`)、特定のプロパティだけを厳選して抽出したり(`Pick`)する技術が必須になる。
これを怠って、コンポーネントごとに似たような型をバラバラに定義し始めると、ベースの仕様変更が入った瞬間に型定義の同期が漏れ、あちこちでバグが爆誕する。フロントエンド・アーキテクトとして、それは絶対に防がなければならない。
—
PickとOmitの基本メカニズム(裏側で何が起きているか)
TypeScriptの `Pick` と `Omit` は、どちらも既存の型から新しい型を「外科手術的に」作り出すためのジェネリック型だ。ブラウザが実行時に何か特別な処理をするわけではないが、コンパイルタイム(TypeScriptの型チェック時)において、エディタの補完と型安全性を担保するための最強の武器となる。
- `Pick
` : 型 `T` の中から、プロパティ `K` のみを抽出する。(必要なものだけ摘み取る) - `Omit
` : 型 `T` の中から、プロパティ `K` 以外を抽出する。(不要なものを除外する)
これらは内部的にはマップ型(Mapped Types)と条件付き型(Conditional Types)の合わせ技で実装されている。TypeScriptのエンジンは、これらの型を受け取ると、既存のオブジェクトの構造から指定されたキーを安全にフィルタリングし、全く新しいオブジェクト型をメモリ上に構築するのだ。
—
現場で即戦力になる実装パターン
百聞は一見にしかず。実際のコードベースを想定したサンプルを見ていこう。
今回は、汎用的な `BaseButton` コンポーネントをベースにして、特定の制約を持たせた `CardButton` を作成するシナリオだ。
import React from ‘react’;
// ==========================================
// 1. ベースとなるコンポーネントの型と実装
// ==========================================
export interface BaseButtonProps {
/ ボタンの中に表示するラベル /
label: string;
/ ボタンの視覚的なバリエーション /
variant: ‘primary’ | ‘secondary’ | ‘danger’;
/ クリック時のイベントハンドラ /
onClick: (event: React.MouseEvent
/ 無効状態かどうか /
disabled?: boolean;
/ 内部的なトラッキングID(ビジネスロジック用) /
trackingId: string;
}
export const BaseButton: React.FC
label,
variant,
onClick,
disabled = false,
trackingId,
}) => {
return (
);
};
// ==========================================
// 2. Omitを用いたプロパティの除外(ラッピング)
// ==========================================
/
- 【ユースケース: Omit】
- カード内に配置する専用ボタンを作りたいとする。
- このカードボタンでは、外部から勝手に `trackingId` を上書きされたくない(内部で自動採番するため)。
- また、デザインの都合上、`variant` は常に ‘primary’ に固定したい。
- BaseButtonProps から ‘trackingId’ と ‘variant’ を除外した型を定義する。
/
export type CardButtonProps = Omit
export const CardButton: React.FC
// 内部で自動的にトラッキングIDを生成・付与し、variantを固定してBaseButtonに委譲する
const handleCardButtonClick = (e: React.MouseEvent
// 独自のトラッキング処理などをここに挟める
console.log(‘Card button clicked, internal tracking executed.’);
props.onClick(e);
};
return (
);
};
// ==========================================
// 3. Pickを用いたプロパティの抽出(限定公開)
// ==========================================
/
- 【ユースケース: Pick】
- さらに、最小限の機能だけを持つミニマルなバッジ用ボタンを別のチームから要求されたとする。
- このボタンでは、ラベルとクリックイベントしか必要なく、variantやdisabled、trackingIdなどは一切不要。
- BaseButtonProps から ‘label’ と ‘onClick’ のみを抽出する。
/
export type MinimalButtonProps = Pick
export const MinimalButton: React.FC
// 内部的にデフォルトのvariantやダミーのtrackingIdを補完してBaseButtonを呼び出す
return (
);
};
—
シニアが教える!実務におけるベストプラクティスと罠
この `Pick` と `Omit`、一見すると万能に見えるが、実務で雑に使い倒すと後々痛い目を見る。シニアとして、いくつか現場の教訓を共有しておこう。
1. 「コンポーネントの肥大化」のサインとして受け取る
`Omit` を使ってあまりにも多くのプロパティを削っている場合(例: 20個あるPropsのうち18個を `Omit` で消すようなケース)、そのベースとなるコンポーネント自体の設計が間違っている可能性が高い。単一責任の原則(SRP)に立ち返り、コンポーネントをアトミックに分割すべきだ。
2. HTMLのネイティブ要素を拡張・制限する場合の注意
Reactでよくあるのが、`React.ComponentPropsWithoutRef<'button'>` から特定のイベントハンドラを除外するパターンだ。
type CustomInputProps = Omit
// 独自にカスタムしたonChangeを定義
onChange: (value: string) => void;
};
このようにネイティブの型と組み合わせることで、開発体験が劇的に向上する。
—
まとめ
`Pick` と `Omit` は、単なるTypeScriptの機能ではない。「コンポーネント間の関係性を美しく保ち、変更に強いアーキテクチャを構築するためのデザインパターン」そのものだ。
DRY原則を意識し、型定義の重複を排除していくことで、君の書くReactコードベースは圧倒的にメンテナンスしやすくなり、チームメンバーからも一目置かれる存在になるはずだ。
今日の帰り道、自分が過去に書いたコンポーネントの型定義を見返して、「あ、ここ `Omit` 使えばもっと綺麗になるな」と思う箇所を探してみてほしい。その積み重ねが、君を真のフロントエンド・スペシャリストへと導く。さて、次のタスクに取り掛かろうか。

コメント