ウェブエンジニア問題集
第13章

ReactとTypeScript — Props・Hooks・イベントの型付け

5
この章の目次開く

この章はReact入門本との接続ポイントです。React本で学んだコンポーネント・props・Hooksを、TypeScriptでどう型付けするかを整理します。

学習者学習者

Reactのpropsって、TypeScriptだとどう型を付けるのが正解なの?毎回 any にしちゃってる…。

Propsの型付け

Propsの型は type または interface で定義し、コンポーネントの引数に渡します。

type ButtonProps = {
  label: string;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
  onClick: () => void;
};
 
function Button({ label, variant = 'primary', disabled, onClick }: ButtonProps) {
  return (
    <button
      className={`btn-${variant}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
}
tsx

オプショナルとデフォルト値

type CardProps = {
  title: string;            // 必須
  subtitle?: string;        // 省略可能(string | undefined)
  maxWidth?: number;        // 省略可能
};
 
// デフォルト値は分割代入で設定
function Card({ title, subtitle, maxWidth = 400 }: CardProps) {
  return (
    <div style={{ maxWidth }}>
      <h2>{title}</h2>
      {subtitle && <p>{subtitle}</p>}
    </div>
  );
}
tsx

childrenの型

含まれるもの使いどころ
React.ReactNode文字列、数値、JSX、配列、null、undefined、boolean最も一般的。ほとんどの場合これ
React.ReactElementJSX要素のみ(文字列やnullは不可)子要素がJSXであることを保証したいとき
string文字列のみテキストだけを受け取りたいとき
type LayoutProps = {
  children: React.ReactNode;
};
 
function Layout({ children }: LayoutProps) {
  return (
    <div className="container">
      <header>ヘッダー</header>
      <main>{children}</main>
    </div>
  );
}
tsx
children の型は迷ったら React.ReactNode を使えばほぼ間違いない。

useStateの型

useState はジェネリクスで型を指定できます。

// 初期値から推論される場合(ジェネリクス不要)
const [count, setCount] = useState(0);         // number
const [name, setName] = useState('');           // string
const [isOpen, setIsOpen] = useState(false);    // boolean
 
// 初期値がnullの場合(ジェネリクスが必要)
const [user, setUser] = useState<User | null>(null);
 
// 配列の場合
const [items, setItems] = useState<string[]>([]);
 
// オブジェクトの場合
type FormData = { name: string; email: string };
const [form, setForm] = useState<FormData>({ name: '', email: '' });
tsx
先生先生

初期値の型から推論できるなら、ジェネリクスを書く必要はない。useState&lt;number&gt;(0) は冗長。null が初期値のときだけ明示的に書こう。

useRefの型

useRef は DOM要素を参照する場合と、値を保持する場合で書き方が変わります。

// DOM要素の参照(初期値は null)
const inputRef = useRef<HTMLInputElement>(null);
 
// 値の保持(初期値は具体的な値)
const countRef = useRef(0);
const timerRef = useRef<ReturnType<typeof setTimeout>>(null);
tsx
// DOM参照の使用例
function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null);
 
  const handleClick = () => {
    inputRef.current?.focus();  // null チェックが必要
  };
 
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>フォーカス</button>
    </>
  );
}
tsx

イベントハンドラの型

よく使うイベント型の一覧です。

イベント使うHTML要素
クリックReact.MouseEvent<HTMLButtonElement>button, div
入力変更React.ChangeEvent<HTMLInputElement>input, select, textarea
フォーム送信React.FormEvent<HTMLFormElement>form
キー入力React.KeyboardEvent<HTMLInputElement>input, div
フォーカスReact.FocusEvent<HTMLInputElement>input
// onChange
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setName(e.target.value);
};
 
// onSubmit
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // フォーム送信処理
};
 
// onClick
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log('clicked at', e.clientX, e.clientY);
};
tsx

コンポーネントの型 — React.FCを使うかどうか

// ❌ React.FC を使うスタイル(現在は非推奨気味)
const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
  return <button onClick={onClick}>{label}</button>;
};
 
// ✅ 関数宣言でpropsの型を直接書くスタイル(推奨)
function Button({ label, onClick }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}
tsx

React.FC は暗黙的に children を受け入れる問題(React 18で修正済み)や、ジェネリクスとの相性の悪さから、関数宣言で直接書くスタイルが主流です。

ジェネリックコンポーネント

リストやセレクトボックスなど、扱うデータの型をコンポーネント利用側が決めるパターンです。

type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
};
 
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}
tsx
// 使う側 — User型が自動推論される
type User = { id: string; name: string; age: number };
 
<List<User>
  items={users}
  renderItem={(user) => <span>{user.name}({user.age}歳)</span>}
  keyExtractor={(user) => user.id}
/>
tsx
型安全なコンポーネント
ジェネリックコンポーネントは、ライブラリやデザインシステムを作る際に強力

実務でよく使うパターン集

コンポーネントpropsを拡張する

// HTMLのbutton属性をすべて引き継ぐ
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant: 'primary' | 'secondary';
};
 
function Button({ variant, className, ...rest }: ButtonProps) {
  return <button className={`btn-${variant} ${className ?? ''}`} {...rest} />;
}
tsx

discriminated union で状態を表現する

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };
 
function UserProfile({ state }: { state: AsyncState<User> }) {
  switch (state.status) {
    case 'idle':    return <p>データを取得してください</p>;
    case 'loading': return <p>読み込み中...</p>;
    case 'success': return <p>{state.data.name}</p>;  // data にアクセス可能
    case 'error':   return <p>エラー: {state.error}</p>;
  }
}
tsx

ちゃんと使うためのポイント

  • Propsの型は type で定義し、コンポーネントの引数に直接書く
  • childrenReact.ReactNode がほぼ万能
  • useState の型は初期値から推論できるなら省略、null 初期値のときだけ明示
  • イベント型はエディタの推論で調べるのが実務的
  • React.FC より関数宣言で直接propsの型を書くスタイルが主流
  • HTML属性の拡張、discriminated union は実務で頻出

次の章では、TypeScriptの実務パターンをさらに掘り下げます。

参考リンク

TypeScriptクイズに挑戦するこの章で学んだTypeScriptの知識を、4択クイズでアウトプットして定着させよう