TypeScript で React コンポーネントの型付け – 実装パターン集
React の型安全性の重要性
TypeScript を使っていても、React コンポーネント開発での型付けが曖昧だと、実行時エラーが発生してしまいます。TypeScript の型付けについて「Props の型定義がどうやって書くのか分からない」「TypeScript での Generic コンポーネントの型定義が複雑」といった悩みを抱えている React 開発者は多いでしょう。
今回は、TypeScript でのProps の基本的な型定義から高度な Generic パターンまで、React コンポーネント開発でよく使う TypeScript 型付けパターンをまとめておきたいと思います。
対象となる方
- React と TypeScript の基本は知っているが、型付けに自信がない方
- よくある型付けミスを避けたい方
- 高度なコンポーネント設計を学びたい方
※ このドキュメントは React 18、TypeScript 4.9+ で書いていきます。
Props の基本的な型定義
interface で Props 型を定義
React コンポーネントの Props は、interface で型を定義します。
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary';
}
export function Button({ label, onClick, disabled, variant = 'primary' }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
type と interface の使い分け
interface:オブジェクト型の場合(ほとんどの Props)type:複雑な型(Union, Intersection)の場合
// interface の場合
interface InputProps {
type: 'text' | 'password' | 'email';
value: string;
onChange: (value: string) => void;
}
// type の場合(Union)
type AlertProps = {
message: string;
} & ({ type: 'error' } | { type: 'success' });
React.FC(Function Component)の型
React.FC は関数コンポーネント用の型ですが、最新では推奨されていません。代わりに、Props 型を直接指定する方が一般的です。
// ❌ 古い書き方
const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};
// ✅ 新しい書き方
export function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}
children の型付け
children: React.ReactNode
コンポーネントが子要素を受け入れる場合、children の型は React.ReactNode で定義します。
interface CardProps {
title: string;
children: React.ReactNode;
}
export function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
children の詳細型
React.ReactNode は最も広い型です。場合によってはより詳細な型を指定します。
| 型 | 説明 | 例 |
| React.ReactNode | すべての子要素を受け入れる(最も広い) | string, JSX, Fragment など |
| React.ReactElement | JSX 要素のみ | <Component />, <div>…</div> |
| string | number | プリミティブ値のみ | テキスト、数値のみ |
イベントハンドラの型付け
React のイベント型
React のイベントハンドラは、React.MouseEvent<HTMLButtonElement> のような専用型を使用します。
interface ButtonProps {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
export function Button({ onClick }: ButtonProps) {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
onClick(e);
};
return <button onClick={handleClick}>Click me</button>;
}
よく使うイベント型
// マウスイベント
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
// フォーム入力イベント
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
// フォーム送信イベント
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
// キーボードイベント
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
// フォーカスイベント
onFocus: (event: React.FocusEvent<HTMLInputElement>) => void;
フォーム要素の型付け
interface InputProps {
value: string;
onChange: (value: string) => void;
}
export function Input({ value, onChange }: InputProps) {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value);
};
return <input value={value} onChange={handleChange} />;
}
Hooks の型付け
useState の型
useState の型は、通常は自動推論されますが、明示的に指定することもできます。
// 型推論を使用(推奨)
const [count, setCount] = useState(0); // number と推論
// 明示的に型を指定
const [count, setCount] = useState<number>(0);
// 複雑な型の場合
interface User {
id: number;
name: string;
}
const [user, setUser] = useState<User | null>(null);
useEffect の型
useEffect は戻り値がクリーンアップ関数(またはなし)です。
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
// クリーンアップ関数
return () => clearInterval(timer);
}, []); // 依存配列
useCallback と useMemo の型
const handleClick: React.MouseEventHandler<HTMLButtonElement> = useCallback(
(event) => {
console.log('clicked');
},
[]
);
interface User {
id: number;
name: string;
}
const processedUsers = useMemo<User[]>(() => {
return users.filter(u => u.id > 0);
}, [users]);
useRef の型
// DOM 要素への ref
const inputRef = useRef<HTMLInputElement>(null);
// 任意の値への ref
const timerRef = useRef<NodeJS.Timeout | null>(null);
export function Input() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus(); // optional chaining
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</>
);
}
Generic コンポーネント
単純な Generic コンポーネント
複数の型に対応するコンポーネントは、Generic を使用します。
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
export function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}
// 使用例
interface User {
id: number;
name: string;
}
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
<List<User>
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
Generic の制約
Generic に型の制約を追加することで、より型安全になります。
// Generic T は { id: string | number } を必ず持つ
interface ListProps<T extends { id: string | number }> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
export function List<T extends { id: string | number }>({
items,
renderItem,
}: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{renderItem(item)}</li>
))}
</ul>
);
}
高度なパターン:Discriminated Union
Discriminated Union で型安全性を向上
複数の異なる Props セットを持つコンポーネントは、Discriminated Union で安全に型付けできます。
type ButtonProps =
| {
variant: 'link';
href: string;
children: React.ReactNode;
}
| {
variant: 'submit' | 'button';
onClick: () => void;
children: React.ReactNode;
};
export function Button(props: ButtonProps) {
if (props.variant === 'link') {
return <a href={props.href}>{props.children}</a>;
}
return (
<button type={props.variant} onClick={props.onClick}>
{props.children}
</button>
);
}
よくある型付けミスと解決策
ミス 1: any の乱用
❌ ダメな例
export function Component({ props }: any) { // any は型安全性が失われる
return <div>{props.name}</div>;
}
✅ 良い例
interface Props {
name: string;
}
export function Component({ name }: Props) {
return <div>{name}</div>;
}
ミス 2: optional chaining の忘れ
❌ ダメな例
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current.focus(); // null の可能性がある
};
✅ 良い例
const focusInput = () => {
inputRef.current?.focus(); // optional chaining で安全
};
ミス 3: children の型を厳しくしすぎ
❌ ダメな例
interface CardProps {
children: React.ReactElement; // Fragment が使えない
}
// これはエラー
<Card>
<>
<div>Item 1</div>
<div>Item 2</div>
</>
</Card>
✅ 良い例
interface CardProps {
children: React.ReactNode; // すべての子要素を受け入れる
}
TypeScript の設定ポイント
tsconfig.json で以下の設定をすると、より厳密な型チェックが可能です。
{
"compilerOptions": {
"strict": true, // 厳密モード
"noImplicitAny": true, // any の暗黙推論を禁止
"strictNullChecks": true, // null/undefined チェック
"strictFunctionTypes": true, // 関数型をより厳密に
"noUnusedLocals": true, // 使わないローカル変数を警告
"noUnusedParameters": true // 使わないパラメータを警告
}
}
まとめ
以上で TypeScript での React コンポーネント型付けを終えたいと思います。
TypeScript を使った React コンポーネント開発での型付けは、最初は煩雑に感じるかもしれませんが、一度習慣になると、開発の効率と安定性が大幅に向上します。TypeScript での Props の型定義、Generic パターン、Discriminated Union などを習得することで、より保守性の高い React コンポーネント設計が実現できます。
「型が邪魔だ」から「TypeScript の型があってよかった」へ、その転換点がきっと来ます。ぜひこのガイドを参考に、TypeScript での型安全な React 開発を目指してください。
