「Reactの基礎 - TSXの基本構文」の版間の差分

提供: MochiuWiki : SUSE, EC, PCB

ページの作成:「== 概要 == TSX (TypeScript XML) は、ReactアプリケーションをTypeScriptで記述するためのファイル形式 (<code>.tsx</code>) である。<br> JavaScriptの構文拡張であるJSXに、TypeScriptの静的型付けを組み合わせることで、UIコンポーネントの構造を宣言的に記述しながら、コンパイル時に型の整合性を検証できる。<br> <br> TSXを使用することにより、以下に示すようなメリッ…」
 
 
8行目: 8行目:
* コンポーネントの戻り値やRefの型を明示することで、意図しない使い方を防止できる。
* コンポーネントの戻り値やRefの型を明示することで、意図しない使い方を防止できる。
<br>
<br>
React 18以降では <code>React.FC</code> からchildrenの暗黙的な型定義が削除され、TypeScript 5.1ではコンポーネントの戻り値型の制約が緩和されるなど、TSXの記述スタイルに関する標準が整理されてきている。<br>
React 18以降では <code>React.FC</code> からchildrenの暗黙的な型定義が削除され、TypeScript 5.1ではコンポーネントの戻り値型の制約が緩和される等、<br>
TSXの記述スタイルに関する標準が整理されてきている。<br>
<br>
<br>
<center>
<center>

2026年2月24日 (火) 16:12時点における最新版

概要

TSX (TypeScript XML) は、ReactアプリケーションをTypeScriptで記述するためのファイル形式 (.tsx) である。
JavaScriptの構文拡張であるJSXに、TypeScriptの静的型付けを組み合わせることで、UIコンポーネントの構造を宣言的に記述しながら、コンパイル時に型の整合性を検証できる。

TSXを使用することにより、以下に示すようなメリットがある。

  • Propsに不正な値を渡した場合にコンパイルエラーとして検出できる。
  • イベントハンドラの引数型が自動推論され、エディタ上で入力補完が効くようになる。
  • コンポーネントの戻り値やRefの型を明示することで、意図しない使い方を防止できる。


React 18以降では React.FC からchildrenの暗黙的な型定義が削除され、TypeScript 5.1ではコンポーネントの戻り値型の制約が緩和される等、
TSXの記述スタイルに関する標準が整理されてきている。

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.jsonjsx オプションを適切に設定する必要がある。
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.FCchildren を自動的に含まなくなったため、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の主なイベント型
イベント型 対象要素の例 用途
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) や 型推論で対応できる場合は、そちらを優先する。


関連情報