目次
TypeScript で React コンポーネントの型付け – 実装パターン集
TypeScript と React の型安全性
TypeScript は React 開発で型安全性を提供し、開発時のバグ検出、コード補完、リファクタリング効率を大幅に向上させます。本記事では、Props 定義からイベントハンドラまで、実装パターンをまとめました。
対象となる方
- React の基本的な使い方は知っている方
- TypeScript を学んでいる、あるいは導入を検討している方
- 型安全性を重視する開発をしたい方
※ このドキュメントは React 18 + TypeScript 5.0 以上を対象とします。
Props の基本的な型定義
インターフェース vs Type
Props の型定義には、interface または type を使用します。一般的には interface を使うことが推奨されていますが、どちらを使っても問題ありません。
// interface を使う場合
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary';
}
// type を使う場合
type ButtonProps = {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary';
};
// コンポーネント実装
function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}
必須プロパティと任意プロパティ
? を付けることで任意プロパティを指定します。デフォルト値を設定することで、呼び出し側の手間を減らせます。
interface CardProps {
title: string; // 必須
description?: string; // 任意
className?: string; // 任意
isLoading?: boolean; // 任意(デフォルト: false)
}
function Card({
title,
description,
className = '',
isLoading = false
}: CardProps) {
return (
<div className={`card ${className}`}>
<h3>{title}</h3>
{description && <p>{description}</p>}
{isLoading && <p>読み込み中...</p>}
</div>
);
}
children の型付け
children プロパティの重要性
children は React の重要な概念です。Layout、Modal、Wrapper など多くのコンポーネントで使用されます。適切に型付けることで、安全かつ使いやすいコンポーネントが実現できます。
import { ReactNode, ReactElement } from 'react';
// ReactNode型:最も広い型。テキスト、要素、配列など何でも受け入れる
interface LayoutProps {
children: ReactNode;
}
function Layout({ children }: LayoutProps) {
return (
<div className="layout">
<header>Header</header>
<main>{children}</main>
<footer>Footer</footer>
</div>
);
}
// ReactElement型:JSX要素のみを受け入れる
interface ModalProps {
children: ReactElement;
title: string;
}
function Modal({ children, title }: ModalProps) {
return (
<div className="modal">
<h2>{title}</h2>
{children}
</div>
);
}
複数の型の children をサポート
import { ReactNode } from 'react';
// 複数の型を受け入れる
interface ContainerProps {
children: ReactNode | ReactNode[];
maxWidth?: number;
}
function Container({ children, maxWidth = 1200 }: ContainerProps) {
return (
<div style={{ maxWidth, margin: '0 auto' }}>
{children}
</div>
);
}
Generic を使ったコンポーネント設計
ジェネリクスの基本
ジェネリクスを使うと、再利用可能で型安全なコンポーネントが作成できます。リスト表示、フォーム管理など多くの場面で活用されます。
// ジェネリックなリストコンポーネント
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string | number;
}
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: User[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
<List<User>
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
複雑なジェネリック型
// ジェネリックなセレクトコンポーネント
interface SelectProps<T, K extends keyof T> {
options: T[];
value?: T[K];
onChange: (selected: T) => void;
getLabel: (item: T) => string;
getKey: (item: T) => string | number;
}
function Select<T, K extends keyof T>({
options,
value,
onChange,
getLabel,
getKey,
}: SelectProps<T, K>) {
return (
<select onChange={(e) => {
const selected = options.find(
(opt) => getKey(opt).toString() === e.target.value
);
if (selected) onChange(selected);
}}>
{options.map((option) => (
<option key={getKey(option)} value={getKey(option)}>
{getLabel(option)}
</option>
))}
</select>
);
}
Hooks の型付け
useCallback の型付け
import { useCallback } from 'react';
interface UserData {
id: number;
name: string;
email: string;
}
function UserManager() {
// コールバック関数の戻り値を明示的に指定
const handleUserUpdate = useCallback(
async (user: UserData): Promise<void> => {
const response = await fetch(`/api/users/${user.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user),
});
if (!response.ok) throw new Error('Failed to update');
},
[]
);
return <div onClick={() => handleUserUpdate({ id: 1, name: 'Alice', email: 'alice@example.com' })}>Update</div>;
}
useEffect の型付け
import { useEffect, useState } from 'react';
interface Article {
id: number;
title: string;
content: string;
}
function ArticleDetail({ articleId }: { articleId: number }) {
const [article, setArticle] = useState<Article | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
const fetchArticle = async () => {
try {
const response = await fetch(`/api/articles/${articleId}`);
if (!response.ok) throw new Error('Article not found');
const data: Article = await response.json();
if (isMounted) setArticle(data);
} catch (err) {
if (isMounted) setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
if (isMounted) setLoading(false);
}
};
fetchArticle();
return () => { isMounted = false; };
}, [articleId]);
if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error}</p>;
return article ? <h1>{article.title}</h1> : null;
}
useRef の型付け
import { useRef } from 'react';
function FormInput() {
// DOM要素への参照
const inputRef = useRef<HTMLInputElement>(null);
// 任意の値への参照
const countRef = useRef<number>(0);
const focusInput = () => {
inputRef.current?.focus();
};
const incrementCount = () => {
if (countRef.current !== null) {
countRef.current++;
console.log(`Count: ${countRef.current}`);
}
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
<button onClick={incrementCount}>Increment</button>
</>
);
}
イベントハンドラの型付け
React.FormEvent と React.ChangeEvent
import { FormEvent, ChangeEvent, useState } from 'react';
interface FormData {
username: string;
email: string;
password: string;
}
function LoginForm() {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
password: '',
});
// 入力変更イベント
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.currentTarget;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
// フォーム送信イベント
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.ok) {
console.log('ログイン成功');
}
} catch (error) {
console.error('ログイン失敗:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
<button type="submit">ログイン</button>
</form>
);
}
クリックイベントと他のイベント
import { MouseEvent, KeyboardEvent } from 'react';
function EventHandlers() {
// クリックイベント
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
console.log('Button clicked');
};
// キーボードイベント
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
console.log('Enter pressed');
}
};
return (
<>
<button onClick={handleClick}>Click me</button>
<input onKeyDown={handleKeyDown} />
</>
);
}
よくある型付けミスと解決策
any 型の使用は避ける
// ❌ 良くない例:any型
function handleData(data: any) {
console.log(data.name); // 実行時にエラーが発生する可能性がある
}
// ✅ 良い例:unknown型とタイプガード
function handleData(data: unknown) {
if (data && typeof data === 'object' && 'name' in data) {
console.log((data as { name: string }).name);
}
}
型の厳密性
// ❌ 良くない例:非常に広い型
interface Props {
data: object;
}
// ✅ 良い例:具体的な型
interface User {
id: number;
name: string;
email: string;
}
interface Props {
data: User;
}
null/undefined のチェック
// ❌ 良くない例:型チェックなし
function displayName(user: { name: string } | null) {
return <p>{user.name}</p>; // エラー:userがnullの可能性
}
// ✅ 良い例:型チェックあり
function displayName(user: { name: string } | null) {
return <p>{user?.name || 'Unknown'}</p>;
}
まとめ
TypeScript により React 開発の品質が大幅に向上します。適切な型定義により以下が実現できます:
- 開発時のバグ検出
- IDE の優秀なコード補完
- リファクタリング時の安全性
- チーム開発での意思疎通
はじめは型定義に手間がかかりますが、長期的にはバグ修正時間を大幅に削減できます。しっかりした型定義でバグを防ぎ、保守性の高い React コンポーネントを開発しましょう。
