Welcome to Tech Athletes | テック・アスリート   Click to listen highlighted text! Welcome to Tech Athletes | テック・アスリート

【2026年版】React × TypeScript の型定義完全ガイド|useState・useEffect・カスタムフックの書き方を徹底解説

「TypeScriptをReactで使いたいけど、型の書き方がわからない」「useStateやuseEffectをTypeScriptで正しく書く方法は?」という疑問を解決します。この記事ではReact × TypeScriptの実践的な書き方をコード例たっぷりで解説します。

環境構築:Vite + React + TypeScript

2026年現在、ReactのTypeScript開発にはViteが最もおすすめです。CRAよりも起動が速く、設定がシンプルです。

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

基本的な型定義

Props の型定義

// interface を使った Props の定義
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;  // ? でオプショナル
  variant?: 'primary' | 'secondary' | 'danger';  // Union型
}

const Button: React.FC<ButtonProps> = ({ 
  label, 
  onClick, 
  disabled = false,
  variant = 'primary'
}) => {
  return (
    <button 
      className={variant}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
};

useState の型定義

import { useState } from 'react';

// プリミティブ型
const [count, setCount] = useState<number>(0);
const [name, setName] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);

// オブジェクト型
interface User {
  id: number;
  name: string;
  email: string;
}

const [user, setUser] = useState<User | null>(null);

// 配列型
const [items, setItems] = useState<string[]>([]);
const [users, setUsers] = useState<User[]>([]);

useEffect の型定義

import { useState, useEffect } from 'react';

interface Post {
  id: number;
  title: string;
  body: string;
}

function PostList() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        if (!response.ok) throw new Error('Network response was not ok');
        const data: Post[] = await response.json();
        setPosts(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : '不明なエラー');
      } finally {
        setLoading(false);
      }
    };

    fetchPosts();
  }, []);

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

useRef の型定義

import { useRef } from 'react';

function TextInput() {
  // DOM要素への参照
  const inputRef = useRef<HTMLInputElement>(null);
  
  // ミュータブルな値(再レンダリングなし)
  const timerRef = useRef<number | null>(null);

  const handleFocus = () => {
    inputRef.current?.focus();  // Optional chaining で安全にアクセス
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>フォーカス</button>
    </div>
  );
}

カスタムフックの型定義

// useFetch カスタムフック
interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then((data: T) => setState({ data, loading: false, error: null }))
      .catch(err => setState({ data: null, loading: false, error: err.message }));
  }, [url]);

  return state;
}

// 使用例
function App() {
  const { data, loading, error } = useFetch<User[]>('/api/users');
  // ...
}

イベントハンドラーの型定義

// よく使うイベントハンドラーの型
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // フォーム送信処理
};

const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(e.currentTarget.textContent);
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === 'Enter') {
    // Enterキー処理
  }
};

よくある TypeScript エラーと解決策

Object is possibly ‘null’

// NG
const value = ref.current.value;  // Error: Object is possibly 'null'

// OK(Optional chaining)
const value = ref.current?.value;

// OK(Non-null assertion)
const value = ref.current!.value;  // nullでないと確信できる場合のみ

Type ‘string | undefined’ is not assignable to type ‘string’

// NG
const name: string = user?.name;  // Error

// OK(デフォルト値)
const name: string = user?.name ?? 'Unknown';

// OK(型ガード)
if (user?.name) {
  const name: string = user.name;  // narrowing されて string 確定
}

まとめ

React × TypeScriptの型定義は最初は戸惑いますが、慣れるとコードの品質と開発体験が劇的に向上します。特にuseState・useRef・カスタムフックの型定義をマスターすることで、大規模アプリケーション開発でのバグを大幅に減らせます。今回紹介したパターンを繰り返し使って身に付けていきましょう。

投稿者 kasata

IT企業でエンジニアとして勤務後、テクノロジー情報メディア「Tech Athletes(テック・アスリート)」を運営。プログラミング、クラウドインフラ(AWS/GCP/Azure)、AI活用、Webサービス開発を専門とする。エンジニア・ビジネスパーソン向けに、実際に使ってみた経験をもとに信頼できる技術情報を発信中。資格:AWS認定ソリューションアーキテクト、Python 3 エンジニア認定試験合格。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

Click to listen highlighted text!