はじめに
Reactでコンポーネント開発をしていると、propsの型を安全に扱いたい場面が多くあります。以前はPropTypesというライブラリが使われていましたが、現在ではTypeScriptが主流となっています。
この記事で分かること
- PropTypesとは何か、なぜ使われていたのか
- PropTypesの限界とTypeScriptへ移行した理由
- 具体的な移行方法とコード例
対象読者
- Reactの基本を理解している方
- PropTypesからTypeScriptへの移行を検討している方
- なぜTypeScriptが推奨されるのか知りたい方
PropTypesとは?
PropTypesの役割
PropTypesは、Reactコンポーネントに渡されるpropsの型を検証するためのライブラリです。JavaScriptには型システムがないため、間違った型のデータが渡されてもコード上では気づきにくいという課題がありました。
PropTypesを使うことで、開発時にpropsの型が正しいかをチェックし、バグを未然に防ぐことができます。
PropTypesが使われていた背景
React 15.5以前は、PropTypesがReact本体に含まれていました。しかし15.5以降は別パッケージとして分離され、React 18までのバージョンで広く使用されてきました。TypeScriptの普及により、現在では非推奨となっています。
PropTypesの基本的な使い方
インストールと設定
まず、PropTypesパッケージをインストールします。
npm install prop-types
基本的な型定義の例
PropTypesを使った基本的なコンポーネントの例を見てみましょう。
import PropTypes from 'prop-types';
function UserCard({ name, age, email }) {
return (
<div>
<h2>{name}</h2>
<p>年齢: {age}</p>
<p>Email: {email}</p>
</div>
);
}
UserCard.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
email: PropTypes.string
};
export default UserCard;
よく使われる型の種類
PropTypesでは以下のような型を定義できます。
import PropTypes from 'prop-types';
Component.propTypes = {
// 基本的な型
stringProp: PropTypes.string,
numberProp: PropTypes.number,
boolProp: PropTypes.bool,
functionProp: PropTypes.func,
arrayProp: PropTypes.array,
objectProp: PropTypes.object,
// 必須項目
requiredProp: PropTypes.string.isRequired,
// 特定の値のみ
enumProp: PropTypes.oneOf(['News', 'Photos']),
// 複数の型を許可
unionProp: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
// 配列の要素の型を指定
arrayOfNumbers: PropTypes.arrayOf(PropTypes.number),
// オブジェクトの形状を定義
objectShape: PropTypes.shape({
name: PropTypes.string,
age: PropTypes.number
})
};
なぜTypeScriptへ移行したのか?
PropTypesの限界
PropTypesにはいくつかの限界があります。
実行時チェックのみ
PropTypesは実行時に型をチェックします。つまり、アプリケーションを実際に動かすまでエラーに気づけません。
IDE補完の弱さ
PropTypesでは、エディタ上での補完機能が十分に働きません。propsに何が渡せるのか、どんな型なのかを確認するには、コンポーネントの定義を見に行く必要があります。
型の再利用性の低さ
PropTypesで定義した型を他のコンポーネントで再利用するのは困難です。型定義を共有しようとすると、コードが煩雑になってしまいます。
TypeScriptのメリット
一方、TypeScriptには以下のようなメリットがあります。
コンパイル時の型チェック
TypeScriptはコードを書いている段階で型エラーを検出します。ビルド前にエラーが分かるため、開発効率が大幅に向上しますね。
強力な補完機能
TypeScriptを使うと、エディタ上でpropsの候補が自動的に表示されます。どんなpropsが必要か、どんな型なのかが一目で分かるため、開発がスムーズになります。
型の再利用と拡張性
TypeScriptでは、型定義を簡単に再利用できます。共通の型を定義して複数のコンポーネントで使い回したり、既存の型を拡張したりすることも容易です。
TypeScriptでの型定義方法
基本的な書き方
TypeScriptでReactコンポーネントの型を定義する基本的な方法を見てみましょう。
type UserCardProps = {
name: string;
age: number;
email: string;
};
function UserCard({ name, age, email }: UserCardProps) {
return (
<div>
<h2>{name}</h2>
<p>年齢: {age}</p>
<p>Email: {email}</p>
</div>
);
}
export default UserCard;
interfaceとtypeの使い分け
TypeScriptではinterfaceとtypeの2つの方法で型を定義できます。
// interface を使った定義
interface UserCardProps {
name: string;
age: number;
email: string;
}
// type を使った定義
type UserCardProps = {
name: string;
age: number;
email: string;
};
基本的にはどちらを使っても問題ありませんが、以下のような使い分けが推奨されます。
- interface: オブジェクトの形状を定義する場合、後から拡張する可能性がある場合
- type: ユニオン型や複雑な型を定義する場合
オプショナルとデフォルト値
オプショナルなpropsは?をつけて定義します。
type UserCardProps = {
name: string;
age: number;
email?: string; // オプショナル
isActive?: boolean; // オプショナル
};
function UserCard({
name,
age,
email = 'not provided', // デフォルト値
isActive = true // デフォルト値
}: UserCardProps) {
return (
<div>
<h2>{name}</h2>
<p>年齢: {age}</p>
<p>Email: {email}</p>
<p>ステータス: {isActive ? 'アクティブ' : '非アクティブ'}</p>
</div>
);
}
PropTypesからTypeScriptへの移行例
実際のコードでPropTypesからTypeScriptへの移行を見てみましょう。
移行前 - PropTypesを使用
import PropTypes from 'prop-types';
function ProductCard({
name,
price,
description,
images,
onAddToCart
}) {
return (
<div className="product-card">
<h3>{name}</h3>
<p className="price">¥{price.toLocaleString()}</p>
<p>{description}</p>
<div className="images">
{images.map((img, index) => (
<img key={index} src={img} alt={name} />
))}
</div>
<button onClick={() => onAddToCart({ name, price })}>
カートに追加
</button>
</div>
);
}
ProductCard.propTypes = {
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
description: PropTypes.string,
images: PropTypes.arrayOf(PropTypes.string).isRequired,
onAddToCart: PropTypes.func.isRequired
};
ProductCard.defaultProps = {
description: '商品説明なし'
};
export default ProductCard;
移行後 - TypeScriptを使用
type ProductCardProps = {
name: string;
price: number;
description?: string;
images: string[];
onAddToCart: (product: { name: string; price: number }) => void;
};
function ProductCard({
name,
price,
description = '商品説明なし',
images,
onAddToCart
}: ProductCardProps) {
return (
<div className="product-card">
<h3>{name}</h3>
<p className="price">¥{price.toLocaleString()}</p>
<p>{description}</p>
<div className="images">
{images.map((img, index) => (
<img key={index} src={img} alt={name} />
))}
</div>
<button onClick={() => onAddToCart({ name, price })}>
カートに追加
</button>
</div>
);
}
export default ProductCard;
移行時のポイント
PropTypesからTypeScriptへ移行する際の主なポイントをまとめます。
より厳密な型定義の例
TypeScriptではPropTypesよりも厳密な型定義が可能です。
// ステータスを文字列リテラル型で定義
type Status = 'pending' | 'approved' | 'rejected';
// ユーザー型を定義
type User = {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
};
// コンポーネントのprops型
type UserStatusCardProps = {
user: User;
status: Status;
onStatusChange: (newStatus: Status) => void;
};
function UserStatusCard({
user,
status,
onStatusChange
}: UserStatusCardProps) {
return (
<div>
<h3>{user.name} ({user.role})</h3>
<p>ステータス: {status}</p>
<select
value={status}
onChange={(e) => onStatusChange(e.target.value as Status)}
>
<option value="pending">保留中</option>
<option value="approved">承認済み</option>
<option value="rejected">却下</option>
</select>
</div>
);
}
この例では、PropTypesでは表現できなかった文字列リテラル型やネストしたオブジェクト型を使って、より安全な型定義を実現しています。
まとめ
TypeScript移行の推奨理由
PropTypesからTypeScriptへの移行が推奨される理由をまとめます。
- 開発時点でのエラー検出: コードを書いている段階で型エラーが分かるため、バグを早期に発見できる
- 優れた開発者体験: エディタの補完機能により、コーディングが効率的になる
- 保守性の向上: 型定義がドキュメントの役割を果たし、コードの意図が明確になる
- 型の再利用: 共通の型定義を複数の箇所で使い回せる
- コミュニティの支援: TypeScriptは活発にメンテナンスされており、今後も発展が期待できる
PropTypesは実行時チェックという制約があり、現代のReact開発においてはTypeScriptの方が適しています。新規プロジェクトでは迷わずTypeScriptを選択し、既存プロジェクトも段階的に移行していくことをおすすめします。