Reactの基礎 - TSXの基本構文
概要
TSX (TypeScript XML) は、ReactアプリケーションをTypeScriptで記述するためのファイル形式 (.tsx) である。
JavaScriptの構文拡張であるJSXに、TypeScriptの静的型付けを組み合わせることで、UIコンポーネントの構造を宣言的に記述しながら、コンパイル時に型の整合性を検証できる。
TSXを使用することにより、以下に示すようなメリットがある。
- Propsに不正な値を渡した場合にコンパイルエラーとして検出できる。
- イベントハンドラの引数型が自動推論され、エディタ上で入力補完が効くようになる。
- コンポーネントの戻り値やRefの型を明示することで、意図しない使い方を防止できる。
React 18以降では React.FC からchildrenの暗黙的な型定義が削除され、TypeScript 5.1ではコンポーネントの戻り値型の制約が緩和される等、
TSXの記述スタイルに関する標準が整理されてきている。
| カテゴリ | 主な構文・型 | 概要 |
|---|---|---|
| Propsの型定義 | interface / type |
コンポーネントが受け取るPropsの型を定義する |
| コンポーネント宣言 | React.FC / 関数宣言 |
型付きコンポーネントの宣言スタイルを使い分ける |
| 状態管理の型付け | useState / useReducer |
状態とアクションに型を付与して安全に管理する |
| イベントハンドラの型 | React.MouseEvent / React.ChangeEvent 等 |
クリックやフォーム入力等のイベントに型を指定する |
| Refの型付け | useRef |
DOM要素やミュータブルな値に型を付与して参照する |
| childrenの型定義 | React.ReactNode / PropsWithChildren |
子要素を受け取るコンポーネントの型を定義する |
| ジェネリックコンポーネント | 型パラメータ (<T>) |
型パラメータにより汎用的なコンポーネントを実装する |
| 型アサーション | as 構文 |
TSXではアングルブラケット構文が使用できないため、as 構文で型を明示する
|
TSXとは
JSXからTSXへ
JSXはJavaScriptの構文拡張であり、UI構造をHTMLに近い形で記述できる。
TSXはこのJSXをTypeScriptで使用するためのファイル形式であり、拡張子は .tsx となる。
.tsx ファイルでは、TypeScriptの型チェックがJSX構文にも適用されるため、以下に示すような恩恵が得られる。
- Propsに渡す値の型チェック
- イベントハンドラの引数型チェック
- コンポーネントの戻り値の型検証
- エディタによる補完・リファクタリング支援
tsconfig.jsonの設定
TSXを使用するには、tsconfig.json の jsx オプションを適切に設定する必要がある。
React 17以降に導入された新しいJSXトランスフォームを利用する場合は、"react-jsx" を指定する。
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
"jsx": "react-jsx" を設定すると、各ファイルで import React from 'react' を記述する必要がなくなる。
"jsx": "react" (旧来の設定) では、全てのTSXファイルにReactのインポートが必要である。
型付きコンポーネントの基本
Propsの型定義
コンポーネントのPropsは interface または type を使用して定義する。
コミュニティの標準では、interface による定義が広く採用されている。
基本的なPropsの型定義例を以下に示す。
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: "primary" | "secondary" | "danger";
}
function Button({ label, onClick, disabled = false, variant = "primary" }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}
? を付けたプロパティはオプショナルとなり、省略可能になる。
デフォルト値は関数引数の分割代入で指定する。
React.FC vs 関数宣言
コンポーネントを定義する方法として、React.FC (または React.FunctionComponent) を使用する方法と、通常の関数宣言を使用する方法がある。
React 18以降、コミュニティの標準は明示的なProps型定義 (通常の関数宣言) に移行している。
2つのアプローチを比較する。
// React.FC を使う方法 (React 18以前でよく見られたスタイル)
const Greeting: React.FC<{ name: string }> = ({ name }) => {
return <p>Hello, {name}!</p>;
};
// 推奨: 明示的なProps型定義を使う方法
interface GreetingProps {
name: string;
}
function Greeting({ name }: GreetingProps) {
return <p>Hello, {name}!</p>;
}
React.FC を使わない理由は以下の通りである。
- React 18以降、
React.FCはchildrenを自動的に含まなくなったため、React 17以前との挙動が変わった。 - ジェネリックコンポーネントに対応しにくい。
- 通常の関数宣言の方がTypeScriptの型推論との親和性が高い。
戻り値の型
コンポーネントの戻り値には JSX.Element または React.ReactNode が使用される。
通常は型推論に任せ、明示的な指定が必要な場合のみ記述する。
// JSX.Element: JSX要素のみを返す場合
function Title(): JSX.Element {
return <h1>タイトル</h1>;
}
// React.ReactNode: nullや文字列も返す可能性がある場合
function MaybeContent({ show }: { show: boolean }): React.ReactNode {
if (!show) return null;
return <p>コンテンツ</p>;
}
// 型推論に任せる方法 (推奨)
function AutoInferred({ text }: { text: string }) {
return <span>{text}</span>;
}
状態管理の型付け
useStateの型
useState は初期値から型を推論できる場合と、明示的に型を指定する必要がある場合がある。
import { useState } from "react";
interface User {
id: number;
name: string;
email: string;
}
function UserForm() {
// 初期値から型推論が可能なケース
const [count, setCount] = useState(0); // number型
const [isLoading, setIsLoading] = useState(false); // boolean型
const [message, setMessage] = useState(""); // string型
// 明示的な型指定が必要なケース
const [user, setUser] = useState<User | null>(null); // null初期値
const [users, setUsers] = useState<User[]>([]); // 空配列
const [selected, setSelected] = useState<number | undefined>(undefined);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>増やす</button>
</div>
);
}
nullや空配列を初期値として使用する場合は、TypeScriptが型を推論できないため、明示的なジェネリック指定が必要となる。
useReducerの型
useReducer では、状態型 (State) とアクション型 (Action) を定義する。
アクション型は判別共用体 (Discriminated Union) を使用して定義するのが推奨パターンである。
import { useReducer } from "react";
// 状態の型定義
interface CounterState {
count: number;
step: number;
}
// アクションの型定義 (判別共用体)
type CounterAction =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" }
| { type: "setStep"; payload: number };
// reducerの実装
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case "increment":
return { ...state, count: state.count + state.step };
case "decrement":
return { ...state, count: state.count - state.step };
case "reset":
return { ...state, count: 0 };
case "setStep":
return { ...state, step: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0, step: 1 });
return (
<div>
<p>Count: {state.count} (Step: {state.step})</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>リセット</button>
<button onClick={() => dispatch({ type: "setStep", payload: 5 })}>Step: 5</button>
</div>
);
}
判別共用体を使用することにより、各アクションの payload に対する型チェックが正確に機能する。
例えば、setStep アクションでは payload が必須となり、他のアクションでは不要であることを型として表現できる。
イベントハンドラの型
一般的なイベント型
Reactは、DOMイベントをラップした独自のSyntheticEvent型を提供している。
下表に、主なイベント型を示す。
| イベント型 | 対象要素の例 | 用途 |
|---|---|---|
React.ChangeEvent<HTMLInputElement> |
input, textarea, select | 入力値の変更 |
React.MouseEvent<HTMLButtonElement> |
button, div, span | マウスクリック・移動 |
React.FormEvent<HTMLFormElement> |
form | フォーム送信 |
React.KeyboardEvent<HTMLInputElement> |
input, textarea | キーボード入力 |
React.FocusEvent<HTMLInputElement> |
input, select | フォーカスの取得・喪失 |
React.DragEvent<HTMLDivElement> |
div, img | ドラッグ&ドロップ |
React.WheelEvent<HTMLDivElement> |
div, canvas | マウスホイール |
イベントハンドラ関数の型定義
イベントハンドラは、JSX属性のインライン定義と、別途関数として定義する2つの方法がある。
import { useState } from "react";
function Form() {
const [inputValue, setInputValue] = useState("");
const [isSubmitted, setIsSubmitted] = useState(false);
// 関数として型を明示して定義
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSubmitted(true);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
console.log("Enterキーが押されました:", inputValue);
}
};
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("クリック位置:", event.clientX, event.clientY);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="テキストを入力"
/>
<button type="submit" onClick={handleButtonClick}>
送信
</button>
{isSubmitted && <p>送信されました: {inputValue}</p>}
</form>
);
}
イベントオブジェクトを使用しない場合は、引数の型注釈を省略しても型推論が機能する。
しかし、イベントオブジェクトのプロパティ (例: event.target.value) にアクセスする場合は、明示的な型指定が必要となる。
Refの型付け
useRefの型
useRef には、DOM要素への参照と、ミュータブルな値の保持という2つの用途がある。
それぞれで型の指定方法が異なる。
import { useRef, useEffect } from "react";
function InputWithFocus() {
// DOM要素への参照: 初期値はnull、型引数にHTML要素型を指定
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
// ミュータブル値の保持: 初期値が null の場合
const timerIdRef = useRef<number | null>(null);
const renderCountRef = useRef<number>(0);
useEffect(() => {
// DOM参照はnullチェックが必要
if (inputRef.current) {
inputRef.current.focus();
}
// ミュータブル値はnullチェック不要 (初期値が0の場合)
renderCountRef.current += 1;
});
const startTimer = () => {
timerIdRef.current = window.setTimeout(() => {
console.log("タイマー発火");
}, 1000);
};
const stopTimer = () => {
if (timerIdRef.current !== null) {
clearTimeout(timerIdRef.current);
timerIdRef.current = null;
}
};
return (
<div ref={divRef}>
<input ref={inputRef} type="text" placeholder="フォーカスされます" />
<button onClick={startTimer}>タイマー開始</button>
<button onClick={stopTimer}>タイマー停止</button>
</div>
);
}
DOM要素への参照では null を初期値とし、使用時に null チェックを行う。
ミュータブル値では、値の型に合わせた初期値を設定する。
childrenの型
React.ReactNode
children プロパティの型として最も汎用的なのが React.ReactNode である。
文字列、数値、JSX要素、配列、null、undefined など、レンダリング可能なあらゆる値を受け付ける。
import { ReactNode } from "react";
interface CardProps {
title: string;
children: ReactNode;
footer?: ReactNode;
}
function Card({ title, children, footer }: CardProps) {
return (
<div className="card">
<div className="card-header">
<h2>{title}</h2>
</div>
<div className="card-body">
{children}
</div>
{footer && (
<div className="card-footer">
{footer}
</div>
)}
</div>
);
}
// 使用例
function App() {
return (
<Card
title="ユーザー情報"
footer={<button>編集</button>}
>
<p>名前: 山田 太郎</p>
<p>メール: yamada@example.com</p>
</Card>
);
}
React.ReactNode はJSX要素だけでなく、文字列や数値も受け付けるため、柔軟なコンポーネント設計が可能となる。
PropsWithChildren
React.PropsWithChildren は、既存のProps型に children?: ReactNode を追加するユーティリティ型である。
React 17以前に React.FC と組み合わせて使われていたパターンだが、現在は明示的に children: ReactNode を定義する方が推奨されている。
import { PropsWithChildren, ReactNode } from "react";
// 推奨: childrenを明示的に定義する方法
interface LayoutProps {
title: string;
children: ReactNode;
}
function Layout({ title, children }: LayoutProps) {
return (
<div>
<h1>{title}</h1>
<main>{children}</main>
</div>
);
}
// 非推奨: PropsWithChildrenを使う方法 (React 17以前のスタイル)
interface OldLayoutProps {
title: string;
}
function OldLayout({ title, children }: PropsWithChildren<OldLayoutProps>) {
return (
<div>
<h1>{title}</h1>
<main>{children}</main>
</div>
);
}
PropsWithChildren を使用せずに明示的に定義するメリットは、以下の通りである。
childrenが必須か任意かをコントロールできる。- Props型を見るだけで
childrenの存在が明確に分かる。 - ソースコードの意図が明示的になる。
ジェネリックコンポーネント
型パラメータを持つコンポーネント
コンポーネントに型パラメータを持たせることにより、汎用的な再利用可能なコンポーネントを定義できる。
function 宣言とアロー関数では、ジェネリックの記述方法が異なる琴に注意が必要である。
import { ReactNode } from "react";
// function宣言によるジェネリックコンポーネント
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
keyExtractor: (item: T) => string;
emptyMessage?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage = "データがありません" }: ListProps<T>) {
if (items.length === 0) {
return <p>{emptyMessage}</p>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// アロー関数によるジェネリックコンポーネント
// .tsx ファイルでは <T> が JSX タグと誤認されるため、<T,> とカンマが必要
const SelectBox = <T extends { id: string; label: string },>(
props: {
options: T[];
value: string;
onChange: (value: string) => void;
}
) => {
return (
<select value={props.value} onChange={(e) => props.onChange(e.target.value)}>
{props.options.map((option) => (
<option key={option.id} value={option.id}>
{option.label}
</option>
))}
</select>
);
};
// 使用例
interface User {
id: string;
name: string;
email: string;
}
function UserList() {
const users: User[] = [
{ id: "1", name: "山田 太郎", email: "yamada@example.com" },
{ id: "2", name: "佐藤 花子", email: "sato@example.com" },
];
return (
<List
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => (
<span>{user.name} ({user.email})</span>
)}
/>
);
}
アロー関数でジェネリックを使う場合の <T,> というカンマは、TSXファイルにおいてJSXタグと区別するために必要な構文である。
extends 制約を使うと、型パラメータが特定のプロパティを持つことを保証できる。
型アサーションとTSX
as構文の使用
TypeScriptでは、型アサーションに2種類の構文がある。
TSXファイルではアングルブラケット構文 (<Type>value) がJSXタグと競合するため使用できない。
代わりに as 構文を使用する。
// 通常の.tsファイルでは両方使用可能
// アングルブラケット構文 (TSXファイルでは使用不可)
// const element = <HTMLInputElement>document.getElementById("input");
// as構文 (TSXファイルで使用する方法)
const element = document.getElementById("input") as HTMLInputElement;
// as構文の様々な使用例
function processData(data: unknown) {
// unknown型から特定の型へのアサーション
const user = data as { name: string; age: number };
console.log(user.name);
}
// イベントターゲットへのアサーション
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const target = event.target as HTMLInputElement;
console.log(target.value);
}
// constアサーション (リテラル型を保持)
const directions = ["up", "down", "left", "right"] as const;
type Direction = typeof directions[number]; // "up" | "down" | "left" | "right"
interface DirectionButtonProps {
direction: Direction;
onClick: (direction: Direction) => void;
}
function DirectionButton({ direction, onClick }: DirectionButtonProps) {
return (
<button onClick={() => onClick(direction)}>
{direction}
</button>
);
}
// satisfies演算子 (TypeScript 4.9以降): 型チェックしつつ型推論を保持
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
} satisfies Record<string, string | number>;
型アサーションは型システムを迂回する操作であるため、過度な使用は避けるべきである。
型ガード (type guard) や 型推論で対応できる場合は、そちらを優先する。
関連情報
- Reactの基礎 - JSXの基本構文
- Reactの基礎 - コンポーネント
- Reactの基礎 - PropsとChildren
- Reactの基礎 - 条件レンダリング
- Reactの基礎 - リストレンダリング