React + TypeScript 基礎
コンポーネントの Props・State・イベントに型を付ける方法を学びます
このチャプターで学ぶこと
- コンポーネントの Props に型を定義できる
- children の型(React.ReactNode)を理解できる
- イベントハンドラの型を正しく付けられる
- useState に型パラメータを指定できる
React + TypeScript 基礎
React コースの Chapter 3 で Props を学びましたが、今度は型安全にしていきましょう。TypeScript を使うと、コンポーネントに渡す Props の型が間違っていればエディタがすぐに教えてくれます。「実行してみたら Props 名をタイポしていた」「数値を渡すべきところに文字列を渡していた」といったミスが劇的に減ります。
コンポーネントの Props に型を定義する
JavaScript の React コンポーネントでは、Props の型はドキュメントやコメントを読まないとわかりませんでした。TypeScript では interface を使って Props の型を明示的に定義します。
// Props の型を interface で定義
interface ProfileProps {
name: string;
age: number;
bio?: string; // ? はオプショナル(省略可能)
}
// 関数の引数に Props の型を指定
function Profile({ name, age, bio }: ProfileProps) {
return (
<div>
<h2>{name}</h2>
<p>{age}歳</p>
{bio && <p>{bio}</p>}
</div>
);
}
// 使う側:型が間違っていればエラーになる
function App() {
return (
<div>
{/* OK */}
<Profile name="山田太郎" age={25} bio="エンジニアです" />
{/* OK:bio はオプショナルなので省略可能 */}
<Profile name="田中花子" age={30} />
{/* エラー!age に文字列を渡している */}
{/* <Profile name="鈴木一郎" age="28" /> */}
{/* エラー!必須の name が抜けている */}
{/* <Profile age={22} /> */}
</div>
);
}
Laravel のフォームリクエストで rules() にバリデーションルールを書くのと似ています。Props の型定義は「このコンポーネントにはこの形式のデータを渡してください」という契約(コントラクト)です。違反すればコンパイル時にエラーになります。
Props の命名規則
Props の interface には コンポーネント名 + Props という名前をつけるのが慣習です。
// コンポーネント名 + Props
interface ButtonProps { /* ... */ }
interface UserCardProps { /* ... */ }
interface SidebarProps { /* ... */ }
React.FC vs 明示的な関数型
React コンポーネントの型付けには2つのスタイルがあります。
// スタイル1:React.FC を使う(非推奨)
const Profile: React.FC<ProfileProps> = ({ name, age }) => {
return <div>{name}</div>;
};
// スタイル2:引数に直接型を指定する(推奨)
function Profile({ name, age }: ProfileProps) {
return <div>{name}</div>;
}
スタイル2(明示的な関数型)を推奨します。 理由は以下の通りです。
React.FCは暗黙的にchildrenを受け付けていた(React 18で修正されたが混乱のもと)- 通常の関数と同じ書き方なので読みやすい
- デフォルト引数との相性がよい
現在のReact + TypeScript のベストプラクティスでは、React.FC を使わず通常の関数で型を指定する方法が主流です。新しいプロジェクトではスタイル2を使いましょう。
デフォルト値付きの Props
JavaScript と同じように、分割代入のデフォルト値を使えます。
interface GreetingProps {
name: string;
greeting?: string; // オプショナル
}
function Greeting({ name, greeting = "こんにちは" }: GreetingProps) {
return <p>{greeting}、{name}さん!</p>;
}
// 使い方
<Greeting name="山田" /> // 「こんにちは、山田さん!」
<Greeting name="田中" greeting="おはよう" /> // 「おはよう、田中さん!」
children の型:React.ReactNode
children を受け取るコンポーネントでは、React.ReactNode 型を使います。
interface CardProps {
title: string;
children: React.ReactNode; // JSX、文字列、数値、null など何でも受け取れる
}
function Card({ title, children }: CardProps) {
return (
<div style={{
border: "1px solid #e2e8f0",
borderRadius: "8px",
padding: "1.5rem",
marginBottom: "1rem",
}}>
<h3 style={{ marginTop: 0 }}>{title}</h3>
{children}
</div>
);
}
// 使い方
function App() {
return (
<Card title="お知らせ">
<p>TypeScriptコースが始まりました!</p>
<ul>
<li>全8チャプター</li>
<li>演習問題付き</li>
</ul>
</Card>
);
}
React.ReactNode vs React.ReactElement
// React.ReactNode — もっとも広い型(推奨)
// JSX要素、文字列、数値、null、undefined、boolean、配列を含む
children: React.ReactNode;
// React.ReactElement — JSX要素のみ(文字列や数値はNG)
children: React.ReactElement;
// string — 文字列のみ
children: string;
ほとんどの場合は React.ReactNode を使えば問題ありません。
children を受け取るつもりがないコンポーネントでは、children を Props の型に含めないでください。TypeScript が「このコンポーネントに children を渡してはいけない」というエラーを出してくれるので、誤った使い方を防げます。
イベントハンドラの型
React コースでイベントハンドリングを学びましたが、TypeScript ではイベントオブジェクトにも型を付けます。
クリックイベント
function Button() {
// イベントハンドラの型を明示する場合
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("クリック位置:", event.clientX, event.clientY);
console.log("ボタン要素:", event.currentTarget.textContent);
};
return <button onClick={handleClick}>クリック</button>;
}
React.MouseEvent<HTMLButtonElement> の HTMLButtonElement は、イベントが発生する要素の型を指定しています。<div> なら HTMLDivElement、<a> なら HTMLAnchorElement です。
入力イベント
フォーム入力のイベントは React.ChangeEvent を使います。
function SearchBox() {
const [query, setQuery] = useState("");
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
};
return (
<input
type="text"
value={query}
onChange={handleChange}
placeholder="検索..."
/>
);
}
フォーム送信イベント
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // ページ遷移を防ぐ
console.log("ログイン:", { email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
placeholder="メールアドレス"
/>
<input
type="password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)}
placeholder="パスワード"
/>
<button type="submit">ログイン</button>
</form>
);
}
インラインのイベントハンドラでは型推論が効く
JSX のイベント属性に直接関数を書く場合、TypeScript がイベントの型を自動推論してくれます。
function App() {
return (
<div>
{/* インラインでは型注釈が不要(自動推論される) */}
<button onClick={(e) => {
// e は自動的に React.MouseEvent<HTMLButtonElement> と推論される
console.log(e.currentTarget);
}}>
クリック
</button>
<input onChange={(e) => {
// e は自動的に React.ChangeEvent<HTMLInputElement> と推論される
console.log(e.target.value);
}} />
</div>
);
}
インラインで書く場合は型推論に任せて、別の変数に切り出す場合は明示的に型を付ける、というのが実用的なアプローチです。
よく使うイベント型の一覧
// マウスイベント
onClick: (e: React.MouseEvent<HTMLElement>) => void
onMouseEnter: (e: React.MouseEvent<HTMLElement>) => void
// フォームイベント
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
// キーボードイベント
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void
// フォーカスイベント
onFocus: (e: React.FocusEvent<HTMLElement>) => void
onBlur: (e: React.FocusEvent<HTMLElement>) => void
コールバック Props の型
親コンポーネントから子コンポーネントに関数を渡すパターンです。
interface TodoItemProps {
id: number;
text: string;
completed: boolean;
onToggle: (id: number) => void; // コールバック関数の型
onDelete: (id: number) => void; // 戻り値は void
}
function TodoItem({ id, text, completed, onToggle, onDelete }: TodoItemProps) {
return (
<li style={{ textDecoration: completed ? "line-through" : "none" }}>
<input
type="checkbox"
checked={completed}
onChange={() => onToggle(id)}
/>
<span>{text}</span>
<button onClick={() => onDelete(id)}>削除</button>
</li>
);
}
// 親コンポーネントでの使い方
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "TypeScriptを学ぶ", completed: false },
{ id: 2, text: "Reactに型をつける", completed: false },
]);
const handleToggle = (id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const handleDelete = (id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
{...todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
);
}
useState に型パラメータを指定する
React コースで学んだ useState をTypeScript で使うとき、型パラメータの指定が重要になります。
型推論で十分なケース
初期値から型が明らかな場合、型パラメータの指定は不要です。
// TypeScript が自動で型を推論する
const [count, setCount] = useState(0); // number
const [name, setName] = useState("ゲスト"); // string
const [isOpen, setIsOpen] = useState(false); // boolean
const [items, setItems] = useState(["React"]); // string[]
型パラメータが必要なケース
初期値だけでは型が決まらない場合に、明示的に指定します。
interface User {
id: number;
name: string;
email: string;
}
// 初期値が null だが、後で User オブジェクトが入る
const [user, setUser] = useState<User | null>(null);
// 空配列の初期値だけでは要素の型がわからない
const [users, setUsers] = useState<User[]>([]);
// ユニオン型の State
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");
useState(null) だけだと TypeScript は null 型と推論してしまい、setUser({ id: 1, ... }) のように値を設定するときにエラーになります。useState<User | null>(null) のように明示的に指定しましょう。
よくあるパターン:APIデータの取得
interface Article {
id: number;
title: string;
body: string;
author: string;
}
function ArticleList() {
// 初期値は空配列だが、Article[] 型を明示
const [articles, setArticles] = useState<Article[]>([]);
const [loading, setLoading] = useState(true); // boolean は推論で十分
const [error, setError] = useState<string | null>(null);
// データ取得の処理(useEffect は次のチャプターで詳しく扱います)
useEffect(() => {
fetch("/api/articles")
.then(res => res.json())
.then((data: Article[]) => {
setArticles(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error}</p>;
return (
<ul>
{articles.map(article => (
<li key={article.id}>
<h3>{article.title}</h3>
<p>{article.body}</p>
<small>著者: {article.author}</small>
</li>
))}
</ul>
);
}
スタイルオブジェクトの型:React.CSSProperties
インラインスタイルに型をつける場合は React.CSSProperties を使います。
interface BadgeProps {
label: string;
color?: string;
style?: React.CSSProperties; // スタイルオブジェクトの型
}
function Badge({ label, color = "#2563eb", style }: BadgeProps) {
// 基本スタイルを定義(型は自動推論される)
const baseStyle: React.CSSProperties = {
display: "inline-block",
padding: "0.25rem 0.75rem",
borderRadius: "999px",
fontSize: "0.875rem",
fontWeight: "bold",
color: "white",
backgroundColor: color,
};
return (
<span style={{ ...baseStyle, ...style }}>
{label}
</span>
);
}
// 使い方
<Badge label="TypeScript" />
<Badge label="React" color="#61dafb" style={{ marginLeft: "0.5rem" }} />
React.CSSProperties を使うと、存在しないCSSプロパティ名やタイポをコンパイル時に検出できます。
ユニオン型を使った Props パターン
Props にユニオン型を使うと、限定された値のみを受け付けるコンポーネントが作れます。
interface AlertProps {
type: "success" | "error" | "warning" | "info";
title: string;
children: React.ReactNode;
}
function Alert({ type, title, children }: AlertProps) {
// 型に応じた色を定義
const colors: Record<AlertProps["type"], string> = {
success: "#10b981",
error: "#ef4444",
warning: "#f59e0b",
info: "#3b82f6",
};
return (
<div style={{
padding: "1rem",
borderLeft: `4px solid ${colors[type]}`,
backgroundColor: `${colors[type]}10`,
borderRadius: "4px",
marginBottom: "1rem",
}}>
<strong>{title}</strong>
<div>{children}</div>
</div>
);
}
// 使い方
<Alert type="success" title="保存完了">データが正常に保存されました。</Alert>
<Alert type="error" title="エラー">入力内容に問題があります。</Alert>
// エラー!"danger" は許可されていない
// <Alert type="danger" title="NG">...</Alert>
Record<AlertProps["type"], string> のように、Props の型から値を取り出して他の型定義に使えます。型の一元管理ができるので、新しい type を追加したときに colors の定義漏れをコンパイラが検出してくれます。
React コースで作った Profile コンポーネントを TypeScript に変換しましょう。
ステップ 1: Props のインターフェースを定義する
interface ProfileProps {
name: string;
bio: string;
imageUrl?: string; // オプショナル
hobbies?: string[]; // オプショナル
onContact?: (name: string) => void; // コールバック(オプショナル)
}ステップ 2: コンポーネントに型を適用する
function Profile({ name, bio, imageUrl, hobbies = [], onContact }: ProfileProps) {
// クリックイベントの型
const handleContactClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
onContact?.(name); // オプショナルチェーンでコールバックを呼ぶ
};
return (
<section style={{ display: "flex", gap: "1.5rem", padding: "1.5rem" }}>
{imageUrl && (
<img
src={imageUrl}
alt={`${name}のプロフィール写真`}
style={{
width: "80px",
height: "80px",
borderRadius: "50%",
objectFit: "cover",
}}
/>
)}
<div>
<h2 style={{ margin: "0 0 0.5rem" }}>{name}</h2>
<p style={{ margin: "0 0 0.75rem", color: "#666" }}>{bio}</p>
{hobbies.length > 0 && (
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
{hobbies.map((hobby) => (
<span
key={hobby}
style={{
background: "#eff6ff",
color: "#2563eb",
padding: "0.2rem 0.75rem",
borderRadius: "999px",
fontSize: "0.875rem",
}}
>
{hobby}
</span>
))}
</div>
)}
{onContact && (
<button onClick={handleContactClick} style={{ marginTop: "0.75rem" }}>
連絡する
</button>
)}
</div>
</section>
);
}ステップ 3: 親コンポーネントで使う
function App() {
const handleContact = (name: string) => {
alert(`${name}さんに連絡を送りました!`);
};
return (
<div>
<Profile
name="山田太郎"
bio="バックエンドエンジニア。Laravel歴5年。"
imageUrl="https://i.pravatar.cc/150?img=1"
hobbies={["Laravel", "TypeScript", "Docker"]}
onContact={handleContact}
/>
<Profile
name="田中花子"
bio="フロントエンドエンジニア。"
hobbies={["React", "Figma"]}
/>
</div>
);
}発展課題: Badge コンポーネントを作って、趣味タグの表示を切り出してみましょう。BadgeProps を定義して、label と color を Props として受け取れるようにしてください。
まとめ
- Props の型は
interfaceで定義し、関数の引数に直接指定する React.FCより明示的な関数型を推奨childrenの型はReact.ReactNodeを使う- イベントハンドラの型は
React.MouseEvent,React.ChangeEvent,React.FormEventなど - インラインのイベントハンドラは型推論で十分。別変数に切り出す場合は明示的に型を付ける
useStateは初期値から型推論されるが、nullや空配列が初期値の場合は型パラメータを指定する- スタイルオブジェクトの型は
React.CSSProperties
次のチャプターでは、useRef、useContext、useReducer やカスタムフックの型定義を学びます。