📘

TypeScript入門 - Reactで使う主な型定義

に公開
2

はじめに

ReactアプリケーションでTypeScriptを使用する際に必要となる、基本的な型定義について解説します。TypeScriptを導入することで、型安全性が向上し、開発体験が大幅に改善されます。

コンポーネントの型定義

関数コンポーネントの基本

Reactの関数コンポーネントには、React.FC(Function Component)型を使う方法と、通常の関数として定義する方法があります。

// React.FCを使用する方法
import React from "react";

type Props = {
  title: string;
  count: number;
};

const Component: React.FC<Props> = ({ title, count }) => {
  return (
    <div>
      <h1>{title}</h1>
      <p>Count: {count}</p>
    </div>
  );
};

// 通常の関数として定義する方法(推奨)
const Component = ({ title, count }: Props): JSX.Element => {
  return (
    <div>
      <h1>{title}</h1>
      <p>Count: {count}</p>
    </div>
  );
};

React v18から、「暗黙的に含まれる」は削除されました。
現在では、React.FCよりも通常の関数として定義する方法が推奨されています。理由は以下の通りです:

  • childrenが暗黙的に含まれない(明示的に定義する必要がある)
  • ジェネリクスの扱いがシンプル
  • 戻り値の型を明示できる

Propsの型定義

// 基本的なProps
type ButtonProps = {
  label: string;
  onClick: () => void;
  disabled?: boolean; // オプショナル
};

// childrenを含むProps
type CardProps = {
  title: string;
  children: React.ReactNode;
};

// HTML要素の属性を継承
type CustomButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant: "primary" | "secondary";
};

const CustomButton = ({ variant, ...props }: CustomButtonProps) => {
  return <button className={variant} {...props} />;
};

Hooksの型定義

useState

import { useState } from "react";

// 型推論が効く場合
const [count, setCount] = useState(0); // number型として推論

// 明示的に型を指定
const [user, setUser] = useState<User | null>(null);

// 複数の型を持つ場合
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");

useRef

import { useRef } from "react";

// DOM要素への参照
const inputRef = useRef<HTMLInputElement>(null);

// 使用例
const focusInput = () => {
  inputRef.current?.focus();
};

return <input ref={inputRef} />;

// 値を保持する場合
const countRef = useRef<number>(0);

useEffect

import { useEffect } from "react";

// 基本的な使い方(型定義は不要)
useEffect(() => {
  // 副作用の処理
  const timer = setTimeout(() => {
    console.log("executed");
  }, 1000);

  // クリーンアップ関数
  return () => {
    clearTimeout(timer);
  };
}, []);

useContext

import { createContext, useContext } from "react";

type Theme = "light" | "dark";

type ThemeContextType = {
  theme: Theme;
  toggleTheme: () => void;
};

// undefinedを許容しない方法
const ThemeContext = createContext<ThemeContextType>({
  theme: "light",
  toggleTheme: () => {},
});

// undefinedを許容する方法(推奨)
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("useTheme must be used within ThemeProvider");
  }
  return context;
};

useReducer

import { useReducer } from "react";

type State = {
  count: number;
  error: string | null;
};

type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "reset" }
  | { type: "error"; payload: string };

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + 1 };
    case "decrement":
      return { ...state, count: state.count - 1 };
    case "reset":
      return { ...state, count: 0 };
    case "error":
      return { ...state, error: action.payload };
    default:
      return state;
  }
};

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { count: 0, error: null });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </div>
  );
};

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

import { ChangeEvent, FormEvent, MouseEvent } from "react";

const Form = () => {
  // input要素のchangeイベント
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  // textarea要素のchangeイベント
  const handleTextAreaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
    console.log(e.target.value);
  };

  // select要素のchangeイベント
  const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
    console.log(e.target.value);
  };

  // formのsubmitイベント
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // フォーム送信処理
  };

  // buttonのclickイベント
  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
    console.log("clicked");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <textarea onChange={handleTextAreaChange} />
      <select onChange={handleSelectChange}>
        <option value="a">A</option>
        <option value="b">B</option>
      </select>
      <button onClick={handleClick}>Submit</button>
    </form>
  );
};

カスタムフックの型定義

import { useState, useEffect } from "react";

// 基本的なカスタムフック
const useCounter = (initialValue: number = 0) => {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount((prev) => prev + 1);
  const decrement = () => setCount((prev) => prev - 1);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
};

// 非同期処理を含むカスタムフック
type FetchState<T> = {
  data: T | null;
  loading: boolean;
  error: Error | null;
};

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

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const data = await response.json();
        setState({ data, loading: false, error: null });
      } catch (error) {
        setState({ data: null, loading: false, error: error as Error });
      }
    };

    fetchData();
  }, [url]);

  return state;
};

// 使用例
type User = {
  id: number;
  name: string;
};

const UserProfile = () => {
  const { data, loading, error } = useFetch<User>("/api/user/1");

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!data) return <div>No data</div>;

  return <div>{data.name}</div>;
};

便利なユーティリティ型

// Partial - すべてのプロパティをオプショナルに
type User = {
  id: number;
  name: string;
  email: string;
};

type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }

// Required - すべてのプロパティを必須に
type RequiredUser = Required<PartialUser>;

// Pick - 特定のプロパティのみを抽出
type UserNameAndEmail = Pick<User, "name" | "email">;
// { name: string; email: string; }

// Omit - 特定のプロパティを除外
type UserWithoutId = Omit<User, "id">;
// { name: string; email: string; }

// Record - キーと値の型を指定
type UserRoles = Record<string, "admin" | "user" | "guest">;
// { [key: string]: 'admin' | 'user' | 'guest'; }

// ComponentPropsWithoutRef - コンポーネントのProps型を取得
type ButtonProps = React.ComponentPropsWithoutRef<"button">;

まとめ

ReactでTypeScriptを使用する際の主な型定義について解説しました。これらの型定義を適切に使用することで:

  • 型安全性が向上し、ランタイムエラーを防げる
  • IDEの補完機能が充実し、開発効率が上がる
  • コードの可読性と保守性が向上する

TypeScriptの型システムは強力ですが、最初から完璧を目指す必要はありません。まずは基本的な型定義から始めて、徐々に高度な型定義を取り入れていくことをお勧めします。

GitHubで編集を提案
株式会社three dots.

Discussion