MENU

TypeScript で React コンポーネントの型付け – 実装パターン集

目次

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 コンポーネントを開発しましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次