【TypeScript】React + TypeScriptの始め方|Props・Hooks・イベント・APIの型定義を完全解説

【TypeScript】React + TypeScriptの始め方|Props・Hooks・イベント・APIの型定義を完全解説 TypeScript

TypeScriptの基本的な型定義を学んだ次のステップとして、多くの開発者が取り組むのがReact + TypeScriptの組み合わせです。Reactは世界で最も使われているUIライブラリであり、TypeScriptとの相性は抜群です。しかし、ReactにはPropsHooksイベントハンドラーContextなど独自の概念が多く、「TypeScriptの型をどう書けばいいのか」で悩む場面が頻繁に訪れます。

この記事では、React + TypeScriptプロジェクトの環境構築から、コンポーネントPropsHooksイベントAPI通信フォーム処理まで、実務で必要になる型定義パターンを体系的に解説します。すべてのコード例はコピーしてそのまま使えるように書かれています。

この記事で学べること

  • React + TypeScriptの環境構築(Vite / Create React App / Next.js)
  • 関数コンポーネントの型定義(React.FC vs 通常の関数)
  • Propsの型定義(interface / children / オプショナル / デフォルト値)
  • イベントハンドラーの型定義(onClick / onChange / onSubmit 全パターン)
  • useState / useEffect / useRef / useReducer / useContext の型定義
  • useMemo / useCallback / カスタムHookの型定義
  • API通信の型定義(fetch / axios / エラーハンドリング)
  • フォーム処理の型定義(制御・非制御 / React Hook Form連携)
  • 実務で使えるコンポーネントパターン集
  • React + TypeScriptでよくあるエラーと対処法

前提知識:この記事はTypeScriptの基本的な型(string, number, interface, ジェネリクスなど)を理解していることを前提としています。TypeScriptの基礎が不安な方は、TypeScript 型の書き方 完全入門から読むことをおすすめします。

スポンサーリンク
  1. React + TypeScriptの環境構築
    1. Viteで作成する(推奨)
    2. Create React App(CRA)で作成する
    3. Next.jsで作成する
    4. 環境構築ツールの比較
    5. tsconfig.jsonの主要設定
  2. 関数コンポーネントの型定義
    1. 通常の関数宣言(推奨)
    2. React.FCを使う方法
    3. React.FC vs 通常の関数の比較
    4. 戻り値の型を明示する場合
  3. Propsの型定義
    1. 基本的なProps型
    2. childrenの型定義
    3. オブジェクト型のProps
    4. Props型の拡張(extends / intersection)
    5. Discriminated Union(判別可能なUnion型)のProps
  4. イベントハンドラーの型定義
    1. 主要なイベント型一覧
    2. onClick の型定義
    3. onChange の型定義
    4. onSubmit の型定義
    5. onKeyDown の型定義
    6. イベントハンドラーをPropsとして渡す
  5. useStateの型定義
    1. 基本: 初期値からの型推論
    2. 明示的な型指定が必要なケース
    3. useStateのセッター関数の型
  6. useEffectの型定義と注意点
    1. 基本パターン
    2. タイマーとイベントリスナーのクリーンアップ
  7. useRefの型定義
    1. DOM要素への参照
    2. よく使うHTML要素の型
    3. 値の保持(再レンダリングを引き起こさない)
  8. useReducerの型定義
    1. 基本パターン: カウンター
    2. 実践パターン: Todoリスト
  9. useContextの型定義
    1. 基本: テーマContextの作成
    2. 実践: 認証Contextの作成
  10. useMemoとuseCallbackの型定義
    1. useMemoの型定義
    2. useCallbackの型定義
  11. カスタムHookの型定義
    1. 基本: useToggle
    2. ジェネリクスHook: useFetch
    3. カスタムHook: useLocalStorage
  12. API通信の型定義
    1. fetch APIの型定義
    2. axiosの型定義
  13. フォーム処理の型定義
    1. 制御コンポーネント(Controlled)
    2. 非制御コンポーネント(Uncontrolled + useRef)
    3. React Hook Form との連携
  14. コンポーネントパターン集
    1. Compound Components(複合コンポーネント)
    2. Polymorphic Components(多態性コンポーネント)
    3. ジェネリクスコンポーネント
  15. React + TypeScriptでよくあるエラーと対処法
    1. 1. Type ‘string’ is not assignable to type ‘…(リテラル型)’
    2. 2. Property ‘children’ does not exist
    3. 3. Cannot find name ‘JSX’ / Cannot find module ‘react’
    4. 4. Type ‘null’ is not assignable to type ‘ReactNode’(React 18)
    5. 5. event.target.value の型エラー
    6. エラー対処の早見表
  16. まとめ
    1. TypeScript完全ガイドシリーズ

React + TypeScriptの環境構築

React + TypeScriptプロジェクトを始める方法は複数あります。2024年現在、最も推奨されるのはViteを使った方法です。主要な3つの方法を紹介します。

Viteで作成する(推奨)

Viteはフランス語で「速い」を意味する、次世代のフロントエンドビルドツールです。開発サーバーの起動が高速で、Hot Module Replacement(HMR)も瞬時に反映されます。

Vite + React + TypeScript
# プロジェクト作成
npm create vite@latest my-react-app -- --template react-ts

# ディレクトリに移動してインストール
cd my-react-app
npm install

# 開発サーバー起動
npm run dev

Viteで作成されるプロジェクトの主要なファイル構成は以下の通りです。

プロジェクト構成
my-react-app/
  src/
    App.tsx          ← メインコンポーネント(.tsx拡張子)
    main.tsx         ← エントリーポイント
    vite-env.d.ts    ← Viteの型定義
  tsconfig.json      ← TypeScript設定
  tsconfig.node.json ← Node.js向けTS設定
  vite.config.ts     ← Vite設定
  package.json

ポイント:Reactコンポーネントを含むファイルの拡張子は.tsx、JSX/TSXを含まない純粋なTypeScriptファイルは.tsを使います。Viteのテンプレートはこれが最初から正しく設定されています。

Create React App(CRA)で作成する

従来から使われてきた公式ツールです。2023年以降は積極的なメンテナンスが行われていないため、新規プロジェクトにはViteの利用を推奨しますが、既存プロジェクトで使われているケースは多いです。

Create React App + TypeScript
npx create-react-app my-react-app --template typescript
cd my-react-app
npm start

Next.jsで作成する

Next.jsはReactベースのフルスタックフレームワークで、サーバーサイドレンダリング(SSR)やファイルベースルーティングが特徴です。プロダクション向けのReact開発では最も人気のある選択肢の一つです。

Next.js + TypeScript
npx create-next-app@latest my-next-app --typescript
cd my-next-app
npm run dev

環境構築ツールの比較

項目 Vite Create React App Next.js
起動速度 非常に高速 やや遅い 高速
SSR対応 プラグインで可能 非対応 標準対応
ルーティング 別途ライブラリ 別途ライブラリ ファイルベース
設定の柔軟性 高い ejectが必要 next.config.jsで可能
学習コスト 低い 低い やや高い
推奨シーン SPA / 一般的な開発 既存PJ保守 プロダクション向け

tsconfig.jsonの主要設定

React + TypeScriptプロジェクトで重要なtsconfig.jsonの設定を確認しましょう。

tsconfig.json(React向けの重要設定)
{
  "compilerOptions": {
    // JSX構文のサポート - Reactでは "react-jsx" を使用
    "jsx": "react-jsx",

    // strictモード - 型安全性を最大限に高める
    "strict": true,

    // ESモジュール相互運用
    "esModuleInterop": true,

    // モジュール解決戦略
    "moduleResolution": "bundler",

    // パスエイリアス(@/ で src/ を参照)
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

注意:"jsx": "react-jsx"はReact 17以降の新しいJSX変換に対応する設定です。この設定により、各ファイルでimport React from 'react'を省略できます。React 16以前を使っている場合は"react"を指定してください。

関数コンポーネントの型定義

React + TypeScriptでコンポーネントを定義する方法は大きく2つあります。通常の関数宣言React.FCです。結論から言うと、現在のReactコミュニティでは通常の関数宣言が推奨されています。

通常の関数宣言(推奨)

最もシンプルで、TypeScriptの型推論を最大限に活用できる方法です。

通常の関数宣言(推奨パターン)
// Props型を定義
interface GreetingProps {
  name: string;
  age?: number;  // オプショナル
}

// 関数宣言でコンポーネントを定義
function Greeting({ name, age }: GreetingProps) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age !== undefined && <p>Age: {age}</p>}
    </div>
  );
}

// アロー関数でもOK
const GreetingArrow = ({ name, age }: GreetingProps) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age !== undefined && <p>Age: {age}</p>}
    </div>
  );
};

React.FCを使う方法

React.FC(React.FunctionComponent)は、以前は公式ドキュメントでも推奨されていた書き方です。

React.FC を使う方法
import { FC } from 'react';

interface GreetingProps {
  name: string;
  age?: number;
}

// FC<Props> でコンポーネントの型を明示
const Greeting: FC<GreetingProps> = ({ name, age }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age !== undefined && <p>Age: {age}</p>}
    </div>
  );
};

React.FC vs 通常の関数の比較

項目 通常の関数 React.FC
children 明示的に定義が必要 React 18で暗黙的childrenは削除
ジェネリクス 自然に書ける 書きにくい
戻り値の型 自動推論(JSX.Element | null) ReactElement | null に制限
defaultProps デフォルト引数で対応 defaultPropsと相性が良い
コミュニティの推奨 推奨 非推奨の流れ

ポイント:React 18以降、React.FCから暗黙的なchildrenの型定義が削除されました。これによりReact.FCを使う主なメリットが薄れたため、通常の関数宣言を使うのが現在のベストプラクティスです。

戻り値の型を明示する場合

通常は型推論に任せますが、戻り値を明示したいケースもあります。

戻り値の型を明示
import { ReactElement } from 'react';

// JSX.Element - 最も一般的
function Greeting({ name }: { name: string }): JSX.Element {
  return <h1>Hello, {name}!</h1>;
}

// ReactElement - より厳密
function Greeting2({ name }: { name: string }): ReactElement {
  return <h1>Hello, {name}!</h1>;
}

// nullを返す可能性がある場合
function ConditionalGreeting({ name }: { name?: string }): JSX.Element | null {
  if (!name) return null;
  return <h1>Hello, {name}!</h1>;
}

Propsの型定義

Propsの型定義はReact + TypeScriptの根幹です。ここでは基本パターンから実務でよく使うパターンまで網羅的に解説します。

基本的なProps型

Props型はinterfaceまたはtypeで定義します。どちらを使っても機能的に大きな違いはありませんが、Reactコミュニティではinterfaceが主流です。

基本的なProps型の定義
// interface で定義(推奨)
interface UserCardProps {
  name: string;               // 必須
  email: string;              // 必須
  age?: number;               // オプショナル
  isActive?: boolean;         // オプショナル
  tags?: string[];            // 配列
  role: 'admin' | 'user' | 'guest'; // リテラル型
  onClick: () => void;        // コールバック関数
}

function UserCard({ name, email, age, isActive = true, tags = [], role, onClick }: UserCardProps) {
  return (
    <div onClick={onClick}>
      <h2>{name}</h2>
      <p>{email}</p>
      {age && <p>Age: {age}</p>}
      <span>{isActive ? 'Active' : 'Inactive'}</span>
      <span>Role: {role}</span>
      {tags.map(tag => <span key={tag}>{tag}</span>)}
    </div>
  );
}

childrenの型定義

childrenはReactコンポーネントのもっとも重要なPropsの一つです。使用する要素の種類に応じて、適切な型を選びましょう。

children の型定義パターン
import { ReactNode, PropsWithChildren } from 'react';

// パターン1: ReactNode(最も汎用的・推奨)
interface CardProps {
  title: string;
  children: ReactNode;  // string, number, JSX, null, undefined, 配列すべてOK
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
}

// パターン2: PropsWithChildren ユーティリティ型
interface LayoutProps {
  sidebar?: ReactNode;
}

// PropsWithChildren<LayoutProps> は { sidebar?: ReactNode; children?: ReactNode } と同等
function Layout({ sidebar, children }: PropsWithChildren<LayoutProps>) {
  return (
    <div>
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}

// パターン3: JSX.Elementのみ(文字列やnullを除外)
interface StrictCardProps {
  children: JSX.Element;
}

// パターン4: 関数children(Render Props)
interface DataProviderProps<T> {
  children: (data: T) => ReactNode;
}
children の型 受け入れる値 使用場面
ReactNode JSX、文字列、数値、null、配列 最も汎用的(推奨)
ReactElement JSX要素のみ JSX要素を必須にしたい場合
JSX.Element JSX要素のみ ReactElementとほぼ同じ
string 文字列のみ テキスト専用コンポーネント
(data: T) => ReactNode 関数(Render Props) データを渡すパターン

オブジェクト型のProps

オブジェクト型のProps
// ネストしたオブジェクトの型
interface Address {
  city: string;
  zipCode: string;
  country: string;
}

interface User {
  id: number;
  name: string;
  address: Address;
}

interface UserProfileProps {
  user: User;
  onEdit: (userId: number) => void;
}

function UserProfile({ user, onEdit }: UserProfileProps) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.address.city}, {user.address.country}</p>
      <button onClick={() => onEdit(user.id)}>Edit</button>
    </div>
  );
}

Props型の拡張(extends / intersection)

Props型の拡張パターン
// ベースProps
interface BaseButtonProps {
  label: string;
  disabled?: boolean;
}

// extends で拡張(interface)
interface PrimaryButtonProps extends BaseButtonProps {
  variant: 'primary';
  size?: 'sm' | 'md' | 'lg';
}

// intersection で拡張(type)
type IconButtonProps = BaseButtonProps & {
  icon: ReactNode;
  iconPosition?: 'left' | 'right';
};

// HTML要素のPropsを継承
interface CustomButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  isLoading?: boolean;
}

// onClick, className, disabled 等すべてのbutton属性が使える
function CustomButton({ variant = 'primary', isLoading, children, ...rest }: CustomButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      disabled={isLoading}
      {...rest}
    >
      {isLoading ? 'Loading...' : children}
    </button>
  );
}

ポイント:React.ButtonHTMLAttributes<HTMLButtonElement>を使うと、HTML標準のbutton属性をすべて自動的に受け入れるボタンコンポーネントが作れます。同様にReact.InputHTMLAttributesReact.AnchorHTMLAttributesなども用意されています。

Discriminated Union(判別可能なUnion型)のProps

条件によって異なるPropsを受け取るコンポーネントに最適なパターンです。

Discriminated Union Props
// リンクとしてのボタン or 通常のボタン
type ButtonProps =
  | {
      as: 'link';
      href: string;
      target?: '_blank' | '_self';
      children: ReactNode;
    }
  | {
      as?: 'button';
      onClick: () => void;
      children: ReactNode;
    };

function Button(props: ButtonProps) {
  if (props.as === 'link') {
    // TypeScriptが自動的に href と target を認識
    return <a href={props.href} target={props.target}>{props.children}</a>;
  }
  // こちらでは onClick が認識される
  return <button onClick={props.onClick}>{props.children}</button>;
}

// 使用例
<Button as="link" href="/about">About</Button>
<Button onClick={() => alert('click')}>Click</Button>
<Button as="link" onClick={() => {}}>Error!</Button>  // コンパイルエラー

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

Reactのイベントハンドラーは合成イベント(SyntheticEvent)を使用します。TypeScriptでは、イベントの種類ごとに適切な型を指定する必要があります。

主要なイベント型一覧

イベント イベント型 主な使用場面
onClick React.MouseEvent<HTMLElement> ボタン、リンクのクリック
onChange React.ChangeEvent<HTMLElement> input, select, textareaの値変更
onSubmit React.FormEvent<HTMLFormElement> フォーム送信
onKeyDown React.KeyboardEvent<HTMLElement> キー入力
onFocus / onBlur React.FocusEvent<HTMLElement> フォーカス操作
onDrag React.DragEvent<HTMLElement> ドラッグ&ドロップ
onScroll React.UIEvent<HTMLElement> スクロール
onMouseEnter / onMouseLeave React.MouseEvent<HTMLElement> マウスホバー

onClick の型定義

onClick の型定義
// インライン - 型は自動推論される
<button onClick={(e) => {
  // e は React.MouseEvent<HTMLButtonElement> と自動推論
  console.log(e.currentTarget.name);
}}>Click</button>

// 別関数で定義する場合は型を明示
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  e.preventDefault();
  console.log('Clicked!');
};

// データを渡す場合
const handleItemClick = (id: number) => (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(`Item ${id} clicked`);
};

// 使用
<button onClick={handleClick}>Click</button>
<button onClick={handleItemClick(42)}>Item 42</button>

onChange の型定義

onChange の型定義(input / select / textarea)
// input要素
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const value = e.target.value;  // string型
  console.log(value);
};

// select要素
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  const value = e.target.value;
  console.log(value);
};

// textarea要素
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  const value = e.target.value;
  console.log(value);
};

// checkbox
const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const checked = e.target.checked;  // boolean型
  console.log(checked);
};

onSubmit の型定義

onSubmit の型定義
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();  // デフォルトの送信を防止

  // FormDataを使って値を取得
  const formData = new FormData(e.currentTarget);
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  console.log({ name, email });
};

// JSX
<form onSubmit={handleSubmit}>
  <input name="name" type="text" />
  <input name="email" type="email" />
  <button type="submit">Submit</button>
</form>

onKeyDown の型定義

onKeyDown の型定義
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === 'Enter') {
    console.log('Enter pressed');
  }
  if (e.key === 'Escape') {
    console.log('Escape pressed');
  }
  // Ctrl + S
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();
    console.log('Save shortcut!');
  }
};

<input onKeyDown={handleKeyDown} />

イベントハンドラーをPropsとして渡す

イベントハンドラーをPropsとして渡す
// 方法1: React.MouseEventHandler を使う
interface ButtonProps {
  onClick: React.MouseEventHandler<HTMLButtonElement>;
}

// 方法2: 関数型を直接書く
interface ButtonProps2 {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

// 方法3: イベントオブジェクト不要なら簡潔に
interface ButtonProps3 {
  onClick: () => void;
}

ポイント:イベントハンドラーの型が分からない場合、まずインラインでonChange={(e) => {}}のように書き、eにマウスカーソルを合わせるとVSCodeが型を表示してくれます。それを別関数の型注釈にコピーすれば確実です。

useStateの型定義

useStateはReactで最も頻繁に使うHookです。TypeScriptでは初期値から型が推論されますが、明示的な型指定が必要になるケースも多いです。

基本: 初期値からの型推論

useState – 初期値からの型推論
import { useState } from 'react';

// 初期値から型が推論される(型引数は不要)
const [count, setCount] = useState(0);          // number
const [name, setName] = useState('');           // string
const [isOpen, setIsOpen] = useState(false);    // boolean
const [items, setItems] = useState(['a', 'b']); // string[]

// 型推論が効いているので型安全
setCount(10);        // OK
setCount('hello');   // エラー: string は number に割り当てられない
setName('Alice');    // OK
setIsOpen(true);      // OK

明示的な型指定が必要なケース

useState – 明示的な型指定
// 1. オブジェクト型
interface User {
  id: number;
  name: string;
  email: string;
}

const [user, setUser] = useState<User>({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
});

// 2. null許容(APIデータなど、初期値がない場合)
const [user2, setUser2] = useState<User | null>(null);

// user2を使う前にnullチェックが必要
if (user2) {
  console.log(user2.name);  // OK: nullチェック後はUser型
}

// 3. Union型(複数の状態を表す)
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');

setStatus('loading');  // OK
setStatus('done');     // エラー: 'done' は Status型に含まれない

// 4. 空配列の初期値(型推論が never[] になるため明示が必要)
const [users, setUsers] = useState<User[]>([]);

// 5. undefined許容
const [selectedId, setSelectedId] = useState<number | undefined>();

注意:useState([])のように空配列を初期値にすると、型がnever[]と推論されます。必ずuseState<User[]>([])のように型引数を指定してください。同様にuseState(null)だけではnull型になるので、useState<User | null>(null)とします。

useStateのセッター関数の型

セッター関数をPropsとして渡す
import { Dispatch, SetStateAction } from 'react';

interface CounterProps {
  count: number;
  setCount: Dispatch<SetStateAction<number>>;
}

function Counter({ count, setCount }: CounterProps) {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>+1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

useEffectの型定義と注意点

useEffectは副作用(API呼び出し、DOMの操作、タイマーなど)を扱うHookです。useEffect自体にジェネリクスパラメータはありませんが、内部で使用する変数やコールバック関数の型定義が重要です。

基本パターン

useEffect の基本パターン
import { useState, useEffect } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let isMounted = true;

    const fetchUser = async () => {
      try {
        setLoading(true);
        const res = await fetch(`/api/users/${userId}`);
        const data: User = await res.json();
        if (isMounted) setUser(data);
      } catch (err) {
        if (isMounted) setError(err instanceof Error ? err.message : 'Unknown error');
      } finally {
        if (isMounted) setLoading(false);
      }
    };

    fetchUser();

    return () => { isMounted = false; };
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!user) return null;

  return <h1>{user.name}</h1>;
}

注意:useEffectのコールバック関数はasync関数にできません。useEffectはvoidまたはクリーンアップ関数(() => void)を返す必要がありますが、async関数はPromiseを返すためです。内部で別のasync関数を定義して呼び出してください。

タイマーとイベントリスナーのクリーンアップ

タイマー・イベントリスナーのクリーンアップ
// タイマーのクリーンアップ
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId: ReturnType<typeof setInterval> = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);

  return <p>{seconds}秒経過</p>;
}

// ウィンドウサイズ監視
function WindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <p>{size.width} x {size.height}</p>;
}

useRefの型定義

useRefには大きく2つの用途があります。DOM要素への参照と、再レンダリングを引き起こさない値の保持です。用途によって型の書き方が異なります。

DOM要素への参照

useRef – DOM要素への参照
import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  const divRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  const handleClick = () => {
    inputRef.current?.focus();
    inputRef.current?.select();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Focus</button>
    </div>
  );
}

よく使うHTML要素の型

HTML要素 型名
<input> HTMLInputElement
<div> HTMLDivElement
<button> HTMLButtonElement
<form> HTMLFormElement
<textarea> HTMLTextAreaElement
<select> HTMLSelectElement
<a> HTMLAnchorElement
<img> HTMLImageElement
<canvas> HTMLCanvasElement

値の保持(再レンダリングを引き起こさない)

useRef – 値の保持
function StopWatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const renderCountRef = useRef(0);  // number型と推論

  const start = () => {
    intervalRef.current = setInterval(() => {
      setTime(prev => prev + 1);
    }, 1000);
  };

  const stop = () => {
    if (intervalRef.current) clearInterval(intervalRef.current);
  };

  renderCountRef.current += 1;

  return (
    <div>
      <p>{time}秒</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

ポイント:useRef<HTMLInputElement>(null)のようにDOM参照で初期値をnullにすると、.currentは読み取り専用(RefObject)になります。一方、useRef<number>(0)のように直接値を初期値にすると、.currentは書き換え可能(MutableRefObject)になります。

useReducerの型定義

useReducerは複雑な状態管理に適したHookです。TypeScriptとの相性が非常に良く、Discriminated Union(判別可能なUnion型)でアクションを定義することで、reducer内で強力な型チェックが働きます。

基本パターン: カウンター

useReducer – 基本パターン
import { useReducer } from 'react';

interface CounterState {
  count: number;
}

type CounterAction =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'set'; payload: number };

function counterReducer(state: CounterState, action: CounterAction): CounterState {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    case 'set':
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'set', payload: 100 })}>Set 100</button>
    </div>
  );
}

実践パターン: Todoリスト

useReducer – Todoリスト(実践パターン)
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
}

type TodoAction =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: number }
  | { type: 'DELETE_TODO'; payload: number }
  | { type: 'SET_FILTER'; payload: TodoState['filter'] }
  | { type: 'CLEAR_COMPLETED' };

let nextId = 1;

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: nextId++, text: action.payload, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(t => t.id === action.payload ? { ...t, completed: !t.completed } : t),
      };
    case 'DELETE_TODO':
      return { ...state, todos: state.todos.filter(t => t.id !== action.payload) };
    case 'SET_FILTER':
      return { ...state, filter: action.payload };
    case 'CLEAR_COMPLETED':
      return { ...state, todos: state.todos.filter(t => !t.completed) };
  }
}

// 使用例
const [state, dispatch] = useReducer(todoReducer, { todos: [], filter: 'all' });
dispatch({ type: 'ADD_TODO', payload: '買い物に行く' });  // OK
dispatch({ type: 'TOGGLE_TODO', payload: 1 });            // OK
dispatch({ type: 'UNKNOWN' });                           // エラー

ポイント:useReducerのAction型にDiscriminated Unionを使うと、switch文の各case内でaction.payloadの型が自動的に絞り込まれます。例えば'ADD_TODO'ではpayloadがstring型、'TOGGLE_TODO'ではnumber型と正確に推論されます。

useContextの型定義

useContextは、Propsを介さずにコンポーネントツリー全体でデータを共有するHookです。TypeScriptで型安全にContextを定義し、カスタムHookと組み合わせて使うのが実務でのベストプラクティスです。

基本: テーマContextの作成

useContext – テーマContextの作成と使用
import { createContext, useContext, useState, ReactNode } from 'react';

// 1. Context値の型を定義
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

// 2. Contextを作成(初期値にnullを使うパターン)
const ThemeContext = createContext<ThemeContextType | null>(null);

// 3. カスタムHookで型安全にアクセス
function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// 4. Providerコンポーネント
function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 5. 使用するコンポーネント
function Header() {
  const { theme, toggleTheme } = useTheme();  // 型安全
  return (
    <header style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </header>
  );
}

// 6. アプリケーションのルート
function App() {
  return (
    <ThemeProvider>
      <Header />
    </ThemeProvider>
  );
}

実践: 認証Contextの作成

認証Context(実践パターン)
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = async (email: string, password: string) => {
    const res = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
    });
    const data: User = await res.json();
    setUser(data);
  };

  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, isAuthenticated: !!user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// 使用例
function ProfilePage() {
  const { user, logout } = useAuth();

  if (!user) return <p>Please login</p>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Role: {user.role}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

ポイント:Context作成時の初期値にnullを使い、カスタムHookでnullチェックを行うパターンが最も安全です。Provider外でうっかり使った場合に、実行時エラーではなく分かりやすいエラーメッセージが表示されます。

useMemoとuseCallbackの型定義

useMemoは計算結果のメモ化、useCallbackは関数のメモ化に使います。どちらも型は自動推論されるため、明示的な型指定が必要になるケースは少ないですが、知っておくと便利です。

useMemoの型定義

useMemo の型定義
import { useMemo } from 'react';

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

function ProductList({ products, category }: { products: Product[]; category: string }) {
  // 型は自動推論: Product[]
  const filteredProducts = useMemo(
    () => products.filter(p => p.category === category),
    [products, category]
  );

  // 型は自動推論: number
  const totalPrice = useMemo(
    () => filteredProducts.reduce((sum, p) => sum + p.price, 0),
    [filteredProducts]
  );

  // 明示的に型を指定することも可能
  const sortedProducts = useMemo<Product[]>(
    () => [...filteredProducts].sort((a, b) => a.price - b.price),
    [filteredProducts]
  );

  return (
    <div>
      <p>合計: {totalPrice}円</p>
      {sortedProducts.map(p => <div key={p.id}>{p.name}: {p.price}円</div>)}
    </div>
  );
}

useCallbackの型定義

useCallback の型定義
import { useCallback, useState } from 'react';

function SearchForm() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);

  // 型は自動推論: (e: React.ChangeEvent<HTMLInputElement>) => void
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setQuery(e.target.value);
    },
    []
  );

  // 引数と戻り値の型を明示
  const handleSearch = useCallback(
    async (searchQuery: string): Promise<void> => {
      const res = await fetch(`/api/search?q=${searchQuery}`);
      const data: string[] = await res.json();
      setResults(data);
    },
    []
  );

  return (
    <div>
      <input value={query} onChange={handleChange} />
      <button onClick={() => handleSearch(query)}>Search</button>
      {results.map((r, i) => <p key={i}>{r}</p>)}
    </div>
  );
}

カスタムHookの型定義

カスタムHookはReactのロジック再利用の主要な手段です。TypeScriptでは、カスタムHookの戻り値の型定義が重要になります。特にタプル型の戻り値とジェネリクスHookのパターンを押さえましょう。

基本: useToggle

カスタムHook – useToggle
// タプル型で戻り値を定義(as const が重要)
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  // as const でタプル型に(なしだと (boolean | () => void)[] になる)
  return [value, toggle, setTrue, setFalse] as const;
}

// 使用例
function Modal() {
  const [isOpen, toggle, open, close] = useToggle();
  // isOpen: boolean, toggle/open/close: () => void

  return (
    <div>
      <button onClick={open}>Open Modal</button>
      {isOpen && (
        <div>
          <p>Modal Content</p>
          <button onClick={close}>Close</button>
        </div>
      )}
    </div>
  );
}

注意:カスタムHookの戻り値を配列にする場合、as constを忘れると(boolean | (() => void))[]というUnion型の配列になってしまいます。as constを付けることで[boolean, () => void, () => void, () => void]というタプル型になり、分割代入で正しい型が得られます。

ジェネリクスHook: useFetch

ジェネリクスHook – useFetch
interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const json: T = await res.json();
      setData(json);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  }, [url]);

  useEffect(() => { fetchData(); }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
}

// 使用例: 型引数でレスポンスの型を指定
interface User { id: number; name: string; }

function UserList() {
  const { data: users, loading, error } = useFetch<User[]>('/api/users');
  // users は User[] | null 型

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {users?.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

カスタムHook: useLocalStorage

useLocalStorage(ジェネリクスHook)
function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) as T : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((prev: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue];
}

// 使用例: 型が自動推論される
const [name, setName] = useLocalStorage('userName', 'Guest'); // string
const [count, setCount] = useLocalStorage('count', 0);          // number
const [settings, setSettings] = useLocalStorage('settings', { theme: 'dark', lang: 'ja' });

API通信の型定義

React + TypeScriptでのAPI通信は、レスポンスの型定義エラーハンドリングが重要です。fetch APIとaxiosの両方のパターンを解説します。

fetch APIの型定義

fetch API の型定義
// APIレスポンスの型
interface ApiResponse<T> {
  data: T;
  message: string;
  status: number;
}

interface User {
  id: number;
  name: string;
  email: string;
}

interface ApiError {
  message: string;
  code: string;
  status: number;
}

// 型安全なfetch関数
async function fetchApi<T>(url: string, options?: RequestInit): Promise<T> {
  const res = await fetch(url, {
    headers: { 'Content-Type': 'application/json' },
    ...options,
  });

  if (!res.ok) {
    const error: ApiError = await res.json();
    throw new Error(error.message);
  }

  const data: ApiResponse<T> = await res.json();
  return data.data;
}

// 使用例
const users = await fetchApi<User[]>('/api/users');  // User[]
const user = await fetchApi<User>('/api/users/1');   // User

// POST
const newUser = await fetchApi<User>('/api/users', {
  method: 'POST',
  body: JSON.stringify({ name: 'Bob', email: 'bob@example.com' }),
});

axiosの型定義

axios の型定義
import axios, { AxiosResponse, AxiosError } from 'axios';

interface User {
  id: number;
  name: string;
  email: string;
}

// GET - 型引数でレスポンスデータ型を指定
const res: AxiosResponse<User[]> = await axios.get<User[]>('/api/users');
const users: User[] = res.data;

// POST
interface CreateUserRequest {
  name: string;
  email: string;
}

const newUser = await axios.post<User>('/api/users', {
  name: 'Alice',
  email: 'alice@example.com',
} satisfies CreateUserRequest);

// エラーハンドリング
try {
  const res = await axios.get<User[]>('/api/users');
} catch (error) {
  if (axios.isAxiosError<ApiError>(error)) {
    // error.response?.data は ApiError 型
    console.error(error.response?.data.message);
    console.error(error.response?.status);
  }
}

ポイント:axiosのisAxiosError<T>は型ガードとして機能し、catch節内でerror.response?.dataの型をTに絞り込みます。fetchの場合はこのような型ガードが標準で提供されていないため、自前で実装する必要があります。

フォーム処理の型定義

Reactのフォーム処理には制御コンポーネント(Controlled)と非制御コンポーネント(Uncontrolled)の2つのアプローチがあります。それぞれの型定義と、実務で人気のReact Hook Formとの連携方法を解説します。

制御コンポーネント(Controlled)

制御コンポーネント – フォーム
interface FormData {
  name: string;
  email: string;
  role: 'admin' | 'user';
  agreeToTerms: boolean;
}

function RegistrationForm() {
  const [formData, setFormData] = useState<FormData>({
    name: '',
    email: '',
    role: 'user',
    agreeToTerms: false,
  });

  // 汎用的なchangeハンドラー
  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
  ) => {
    const { name, value, type } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox'
        ? (e.target as HTMLInputElement).checked
        : value,
    }));
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={formData.name} onChange={handleChange} />
      <input name="email" type="email" value={formData.email} onChange={handleChange} />
      <select name="role" value={formData.role} onChange={handleChange}>
        <option value="user">User</option>
        <option value="admin">Admin</option>
      </select>
      <label>
        <input name="agreeToTerms" type="checkbox" checked={formData.agreeToTerms} onChange={handleChange} />
        利用規約に同意
      </label>
      <button type="submit">登録</button>
    </form>
  );
}

非制御コンポーネント(Uncontrolled + useRef)

非制御コンポーネント – useRef
function UncontrolledForm() {
  const nameRef = useRef<HTMLInputElement>(null);
  const emailRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const name = nameRef.current?.value ?? '';
    const email = emailRef.current?.value ?? '';
    console.log({ name, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} defaultValue="" />
      <input ref={emailRef} type="email" defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

React Hook Form との連携

React Hook Form + TypeScript
import { useForm, SubmitHandler } from 'react-hook-form';

// フォームのデータ型を定義
interface LoginFormInputs {
  email: string;
  password: string;
  rememberMe: boolean;
}

function LoginForm() {
  // ジェネリクスでフォームデータの型を指定
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginFormInputs>({
    defaultValues: {
      email: '',
      password: '',
      rememberMe: false,
    },
  });

  // SubmitHandler<T> で送信ハンドラの型を定義
  const onSubmit: SubmitHandler<LoginFormInputs> = async (data) => {
    // data は LoginFormInputs 型(型安全)
    console.log(data.email, data.password, data.rememberMe);
    await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("email", {
          required: "メールアドレスは必須です",
          pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, message: "無効なメールアドレスです" },
        })}
      />
      {errors.email && <span>{errors.email.message}</span>}

      <input
        type="password"
        {...register("password", {
          required: "パスワードは必須です",
          minLength: { value: 8, message: "8文字以上必要です" },
        })}
      />
      {errors.password && <span>{errors.password.message}</span>}

      <label>
        <input type="checkbox" {...register("rememberMe")} />
        ログイン状態を保持
      </label>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '送信中...' : 'ログイン'}
      </button>
    </form>
  );
}

コンポーネントパターン集

実務で頻出するReact + TypeScriptのコンポーネントパターンを紹介します。

Compound Components(複合コンポーネント)

親子コンポーネントが暗黙的に状態を共有するパターンです。UIライブラリで頻繁に使われます。

Compound Components パターン
interface AccordionContextType {
  activeIndex: number | null;
  toggleIndex: (index: number) => void;
}

const AccordionContext = createContext<AccordionContextType | null>(null);

// 親コンポーネント
function Accordion({ children }: { children: ReactNode }) {
  const [activeIndex, setActiveIndex] = useState<number | null>(null);
  const toggleIndex = (index: number) => {
    setActiveIndex(prev => prev === index ? null : index);
  };
  return (
    <AccordionContext.Provider value={{ activeIndex, toggleIndex }}>
      {children}
    </AccordionContext.Provider>
  );
}

// 子コンポーネント
function AccordionItem({ index, title, children }: {
  index: number;
  title: string;
  children: ReactNode;
}) {
  const ctx = useContext(AccordionContext);
  if (!ctx) throw new Error('AccordionItem must be inside Accordion');
  const isOpen = ctx.activeIndex === index;
  return (
    <div>
      <button onClick={() => ctx.toggleIndex(index)}>{title}</button>
      {isOpen && <div>{children}</div>}
    </div>
  );
}

// サブコンポーネントを親に付与
Accordion.Item = AccordionItem;

// 使用例
<Accordion>
  <Accordion.Item index={0} title="Section 1">Content 1</Accordion.Item>
  <Accordion.Item index={1} title="Section 2">Content 2</Accordion.Item>
</Accordion>

Polymorphic Components(多態性コンポーネント)

asプロパティでレンダリングするHTML要素を切り替えるパターンです。ChakraUIやMaterial UIなどで使われています。

Polymorphic Components
import { ElementType, ComponentPropsWithoutRef, ReactNode } from 'react';

// as プロパティの型定義
type BoxProps<C extends ElementType> = {
  as?: C;
  children?: ReactNode;
} & ComponentPropsWithoutRef<C>;

function Box<C extends ElementType = 'div'>({ as, children, ...rest }: BoxProps<C>) {
  const Component = as || 'div';
  return <Component {...rest}>{children}</Component>;
}

// 使用例: as によって型が自動的に切り替わる
<Box>Default div</Box>
<Box as="section">Section</Box>
<Box as="a" href="/about">Link</Box>           // href が使える
<Box as="button" onClick={handleClick}>Button</Box>  // onClick が使える
<Box as="a" onClick={handleClick}>Error</Box>      // OK: a要素にもonClickはある

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

データの型をコンポーネント利用時に決定するパターンです。リスト表示やセレクトボックスに便利です。

ジェネリクスコンポーネント
// ジェネリクスなリストコンポーネント
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => ReactNode;
  keyExtractor: (item: T) => string | number;
  emptyMessage?: string;
}

function List<T>({ items, renderItem, keyExtractor, emptyMessage = 'No items' }: 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>
  );
}

// 使用例: User型で使用
interface User { id: number; name: string; }

<List<User>
  items={users}
  keyExtractor={user => user.id}
  renderItem={user => <span>{user.name}</span>}
  emptyMessage="ユーザーがいません"
/>

React + TypeScriptでよくあるエラーと対処法

React + TypeScriptの開発では、独特のエラーに遭遇します。ここでは頻出するエラーとその対処法をまとめます。

1. Type ‘string’ is not assignable to type ‘…(リテラル型)’

リテラル型のエラーと対処
interface ButtonProps {
  variant: 'primary' | 'secondary';
}

// エラー: 'string' は 'primary' | 'secondary' に割り当てられない
const variant = 'primary';  // string型と推論される
<Button variant={variant} />  // エラー!

// 対処法1: as const
const variant1 = 'primary' as const;  // 'primary' リテラル型
<Button variant={variant1} />  // OK

// 対処法2: 型注釈
const variant2: ButtonProps['variant'] = 'primary';
<Button variant={variant2} />  // OK

// 対処法3: satisfies(TypeScript 4.9+)
const config = {
  variant: 'primary',
} satisfies ButtonProps;  // 型チェック + リテラル型を保持

2. Property ‘children’ does not exist

children が存在しないエラー
// エラー: CardProps に children が定義されていない
interface CardProps {
  title: string;
}

function Card({ title, children }: CardProps) { ... } // エラー!

// 対処法1: children を明示的に追加
interface CardProps {
  title: string;
  children: ReactNode;
}

// 対処法2: PropsWithChildren を使う
function Card({ title, children }: PropsWithChildren<{ title: string }>) { ... }

3. Cannot find name ‘JSX’ / Cannot find module ‘react’

モジュール解決エラーの対処
// 原因: @types/react がインストールされていない
// 対処:
npm install --save-dev @types/react @types/react-dom

// tsconfig.json で jsx が正しく設定されているか確認
{
  "compilerOptions": {
    "jsx": "react-jsx"  // React 17+
  }
}

4. Type ‘null’ is not assignable to type ‘ReactNode’(React 18)

React 18 の型エラー対処
// React 18 で @types/react を最新にすると発生することがある
// 対処: @types/react のバージョンを確認・更新
npm install @types/react@latest @types/react-dom@latest

// 条件付きレンダリングの型エラー
function Component({ show }: { show: boolean }) {
  // エラーになる場合
  return show && <div>Content</div>;  // boolean | JSX.Element

  // 対処: 三項演算子を使う
  return show ? <div>Content</div> : null;
}

5. event.target.value の型エラー

event.target の型エラー対処
// エラー: Property 'value' does not exist on type 'EventTarget'
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  // e.target.value は OK(ChangeEvent<HTMLInputElement> なので)
  console.log(e.target.value);
};

// よくある間違い: イベント型のジェネリクスを忘れる
const badHandler = (e: React.ChangeEvent) => {
  e.target.value;  // エラー: EventTarget に value がない
};

// 対処: ジェネリクスで要素型を指定する
const goodHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
  e.target.value;  // OK
};

エラー対処の早見表

エラー 原因 対処法
not assignable to type '...(リテラル)' string がリテラル型に推論されない as const または型注釈
Property 'children' does not exist Props型にchildrenが未定義 ReactNodeを追加
Cannot find module 'react' 型定義パッケージ未インストール @types/reactをインストール
not assignable to 'never' useState([])でnever[]に推論 型引数を明示<T[]>([])
'value' does not exist on EventTarget イベント型のジェネリクス省略 ChangeEvent<HTMLInputElement>
async function in useEffect useEffectにasync関数を直接渡した 内部で別のasync関数を定義

まとめ

この記事では、React + TypeScriptの環境構築から、コンポーネント・Props・Hooks・イベント・API通信・フォーム処理・コンポーネントパターンまで、実務で必要な型定義を体系的に解説しました。

この記事のポイント

  • 環境構築はVite + react-tsテンプレートが最速
  • コンポーネントは通常の関数宣言が推奨(React.FCは非推奨の流れ)
  • Props型はinterfaceで定義し、HTML属性はComponentPropsWithoutRefで継承
  • イベントハンドラーはReact.ChangeEvent<要素型>の形式で型を指定
  • useStateの空配列は型引数を必ず明示useState<T[]>([])
  • useRefのDOM参照はuseRef<HTML要素型>(null)で初期化
  • useReducerはDiscriminated UnionでAction型を定義すると型安全
  • useContextはnull初期値 + カスタムHookのパターンが安全
  • カスタムHookのタプル戻り値にはas constを忘れずに
  • API通信はジェネリクス関数でレスポンス型を指定

TypeScript完全ガイドシリーズ

TypeScriptを基礎から体系的に学びたい方は、以下のシリーズ記事もあわせてご覧ください。

# 記事タイトル 内容
1 型の書き方 完全入門 基本型、オブジェクト型、配列、Union型など
2 関数の型定義 完全ガイド パラメータ型、戻り値型、オーバーロードなど
3 クラスの型定義 完全ガイド クラス型、抽象クラス、アクセス修飾子など
4 ジェネリクス完全ガイド 型パラメータ、制約、条件型、ユーティリティ型
5 よくあるエラーと解決方法 型エラーの読み方、頻出エラー20選
6 tsconfig.json 完全ガイド コンパイラオプション、プロジェクト設定
7 React + TypeScript 完全ガイド(この記事) Props・Hooks・イベント・API・パターン集

React + TypeScriptの型定義は最初は難しく感じるかもしれませんが、パターンを覚えてしまえば一貫した書き方ができるようになります。この記事のコード例をベースに、実際のプロジェクトで型定義を実践してみてください。