MENU

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

目次

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

React の型安全性の重要性

TypeScript を使っていても、React コンポーネント開発での型付けが曖昧だと、実行時エラーが発生してしまいます。TypeScript の型付けについて「Props の型定義がどうやって書くのか分からない」「TypeScript での Generic コンポーネントの型定義が複雑」といった悩みを抱えている React 開発者は多いでしょう。

今回は、TypeScript でのProps の基本的な型定義から高度な Generic パターンまで、React コンポーネント開発でよく使う TypeScript 型付けパターンをまとめておきたいと思います。

対象となる方

  • React と TypeScript の基本は知っているが、型付けに自信がない方
  • よくある型付けミスを避けたい方
  • 高度なコンポーネント設計を学びたい方

 ※ このドキュメントは React 18、TypeScript 4.9+ で書いていきます。

Props の基本的な型定義

interface で Props 型を定義

React コンポーネントの Props は、interface で型を定義します。

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary';
}

export function Button({ label, onClick, disabled, variant = 'primary' }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

type と interface の使い分け

  • interface:オブジェクト型の場合(ほとんどの Props)
  • type:複雑な型(Union, Intersection)の場合
// interface の場合
interface InputProps {
  type: 'text' | 'password' | 'email';
  value: string;
  onChange: (value: string) => void;
}

// type の場合(Union)
type AlertProps = {
  message: string;
} & ({ type: 'error' } | { type: 'success' });

React.FC(Function Component)の型

React.FC は関数コンポーネント用の型ですが、最新では推奨されていません。代わりに、Props 型を直接指定する方が一般的です。

// ❌ 古い書き方
const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
  return <button onClick={onClick}>{label}</button>;
};

// ✅ 新しい書き方
export function Button({ label, onClick }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

children の型付け

children: React.ReactNode

コンポーネントが子要素を受け入れる場合、children の型は React.ReactNode で定義します。

interface CardProps {
  title: string;
  children: React.ReactNode;
}

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

children の詳細型

React.ReactNode は最も広い型です。場合によってはより詳細な型を指定します。

型説明例
React.ReactNodeすべての子要素を受け入れる(最も広い)string, JSX, Fragment など
React.ReactElementJSX 要素のみ<Component />, <div>…</div>
string | numberプリミティブ値のみテキスト、数値のみ

イベントハンドラの型付け

React のイベント型

React のイベントハンドラは、React.MouseEvent<HTMLButtonElement> のような専用型を使用します。

interface ButtonProps {
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}

export function Button({ onClick }: ButtonProps) {
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    onClick(e);
  };

  return <button onClick={handleClick}>Click me</button>;
}

よく使うイベント型

// マウスイベント
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;

// フォーム入力イベント
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;

// フォーム送信イベント
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;

// キーボードイベント
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;

// フォーカスイベント
onFocus: (event: React.FocusEvent<HTMLInputElement>) => void;

フォーム要素の型付け

interface InputProps {
  value: string;
  onChange: (value: string) => void;
}

export function Input({ value, onChange }: InputProps) {
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    onChange(event.target.value);
  };

  return <input value={value} onChange={handleChange} />;
}

Hooks の型付け

useState の型

useState の型は、通常は自動推論されますが、明示的に指定することもできます。

// 型推論を使用(推奨)
const [count, setCount] = useState(0); // number と推論

// 明示的に型を指定
const [count, setCount] = useState<number>(0);

// 複雑な型の場合
interface User {
  id: number;
  name: string;
}

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

useEffect の型

useEffect は戻り値がクリーンアップ関数(またはなし)です。

useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);

  // クリーンアップ関数
  return () => clearInterval(timer);
}, []); // 依存配列

useCallback と useMemo の型

const handleClick: React.MouseEventHandler<HTMLButtonElement> = useCallback(
  (event) => {
    console.log('clicked');
  },
  []
);

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

const processedUsers = useMemo<User[]>(() => {
  return users.filter(u => u.id > 0);
}, [users]);

useRef の型

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

// 任意の値への ref
const timerRef = useRef<NodeJS.Timeout | null>(null);

export function Input() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus(); // optional chaining
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>Focus</button>
    </>
  );
}

Generic コンポーネント

単純な Generic コンポーネント

複数の型に対応するコンポーネントは、Generic を使用します。

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
}

export 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 = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
];

<List<User>
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

Generic の制約

Generic に型の制約を追加することで、より型安全になります。

// Generic T は { id: string | number } を必ず持つ
interface ListProps<T extends { id: string | number }> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

export function List<T extends { id: string | number }>({
  items,
  renderItem,
}: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

高度なパターン:Discriminated Union

Discriminated Union で型安全性を向上

複数の異なる Props セットを持つコンポーネントは、Discriminated Union で安全に型付けできます。

type ButtonProps =
  | {
      variant: 'link';
      href: string;
      children: React.ReactNode;
    }
  | {
      variant: 'submit' | 'button';
      onClick: () => void;
      children: React.ReactNode;
    };

export function Button(props: ButtonProps) {
  if (props.variant === 'link') {
    return <a href={props.href}>{props.children}</a>;
  }

  return (
    <button type={props.variant} onClick={props.onClick}>
      {props.children}
    </button>
  );
}

よくある型付けミスと解決策

ミス 1: any の乱用

❌ ダメな例

export function Component({ props }: any) {  // any は型安全性が失われる
  return <div>{props.name}</div>;
}

✅ 良い例

interface Props {
  name: string;
}

export function Component({ name }: Props) {
  return <div>{name}</div>;
}

ミス 2: optional chaining の忘れ

❌ ダメな例

const inputRef = useRef<HTMLInputElement>(null);

const focusInput = () => {
  inputRef.current.focus(); // null の可能性がある
};

✅ 良い例

const focusInput = () => {
  inputRef.current?.focus(); // optional chaining で安全
};

ミス 3: children の型を厳しくしすぎ

❌ ダメな例

interface CardProps {
  children: React.ReactElement; // Fragment が使えない
}

// これはエラー
<Card>
  <>
    <div>Item 1</div>
    <div>Item 2</div>
  </>
</Card>

✅ 良い例

interface CardProps {
  children: React.ReactNode; // すべての子要素を受け入れる
}

TypeScript の設定ポイント

tsconfig.json で以下の設定をすると、より厳密な型チェックが可能です。

{
  "compilerOptions": {
    "strict": true,                      // 厳密モード
    "noImplicitAny": true,               // any の暗黙推論を禁止
    "strictNullChecks": true,            // null/undefined チェック
    "strictFunctionTypes": true,         // 関数型をより厳密に
    "noUnusedLocals": true,              // 使わないローカル変数を警告
    "noUnusedParameters": true           // 使わないパラメータを警告
  }
}

まとめ

以上で TypeScript での React コンポーネント型付けを終えたいと思います。

TypeScript を使った React コンポーネント開発での型付けは、最初は煩雑に感じるかもしれませんが、一度習慣になると、開発の効率と安定性が大幅に向上します。TypeScript での Props の型定義、Generic パターン、Discriminated Union などを習得することで、より保守性の高い React コンポーネント設計が実現できます。

「型が邪魔だ」から「TypeScript の型があってよかった」へ、その転換点がきっと来ます。ぜひこのガイドを参考に、TypeScript での型安全な React 開発を目指してください。

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