第1回

ReactコンポーネントのProps型定義:基本から実践パターンまで

TypeScriptでReactのprops型を正しく定義する方法を解説。必須・省略可能・デフォルト値・children・ユニオン型・discriminated unionまで、実務で使うパターンを実例付きで網羅します。

·8分で読める

このシリーズについて

「TypeScript × React 実践ガイド」は、TypeScriptをReact開発で正しく使いこなすための実践シリーズです。型エラーに悩まない・補完が効く・チームで安全に開発できるコードの書き方を実例中心に解説します。

テーマ
第1回 Props の型定義(本記事)
第2回 イベントハンドラーの型
第3回 useState・useRef の型定義

基本形:type で定義する

type ButtonProps = {
  label: string
  onClick: () => void
}

const Button = ({ label, onClick }: ButtonProps) => (
  <button onClick={onClick}>{label}</button>
)

interface でも書けますが、React の props では type が一般的です。交差型(&)や条件型など型の表現力が高く、一貫して type を使うとコードが統一されます。

省略可能なprops:? をつける

type CardProps = {
  title: string       // 必須
  subtitle?: string   // 省略可能
  count?: number
}

const Card = ({ title, subtitle, count = 0 }: CardProps) => (
  <div>
    <h2>{title}</h2>
    {subtitle && <p>{subtitle}</p>}
    <span>{count}</span>
  </div>
)

? をつけると型は string | undefined になります。count = 0 のようにデフォルト値を指定すると、undefined が来ても安全に使えます。

children の型定義

import { type ReactNode } from 'react'

type PanelProps = {
  title: string
  children: ReactNode  // JSX・文字列・配列など何でも受け取れる
}

const Panel = ({ title, children }: PanelProps) => (
  <div className="panel">
    <h3>{title}</h3>
    <div>{children}</div>
  </div>
)

children の型は基本的に ReactNode を使います。ReactElement は単一のJSX要素のみ、ReactNode は文字列・数値・null なども含む広い型です。

ユニオン型でバリアントを表現する

ボタンのサイズや種類など、決まった値のどれかを取るpropsに使います。

type ButtonProps = {
  label: string
  variant?: 'primary' | 'secondary' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
}

const Button = ({ label, variant = 'primary', size = 'md' }: ButtonProps) => {
  const variantClass = {
    primary: 'bg-blue-600 text-white',
    secondary: 'bg-gray-200 text-gray-900',
    ghost: 'bg-transparent text-blue-600',
  }[variant]

  return <button className={variantClass}>{label}</button>
}

Discriminated Union:条件によってpropsが変わるとき

「アイコンを表示する場合は icon が必須、そうでなければ不要」のような場合に使います。

type AlertProps =
  | { type: 'info'; message: string }
  | { type: 'error'; message: string; code: number }  // errorのときだけcodeが必要
  | { type: 'success'; message: string }

const Alert = (props: AlertProps) => {
  if (props.type === 'error') {
    // このブロック内ではprops.codeが使える(型が絞り込まれる)
    return <div className="error">{props.message}(コード: {props.code})</div>
  }
  return <div className={props.type}>{props.message}</div>
}

HTML要素のpropsを継承する

既存のHTML要素のpropsをすべて受け取りつつ、独自のpropsを追加する場合です。

import { type ComponentPropsWithoutRef } from 'react'

// HTML <button> のすべてのprops + label
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
  label: string
  isLoading?: boolean
}

const Button = ({ label, isLoading, disabled, ...rest }: ButtonProps) => (
  <button disabled={isLoading || disabled} {...rest}>
    {isLoading ? '読み込み中...' : label}
  </button>
)

// 使う側はHTMLのbuttonと同じ属性も使える
<Button label="送信" type="submit" aria-label="フォームを送信" />

Refを転送するコンポーネント

import { forwardRef, type ComponentPropsWithRef } from 'react'

type InputProps = ComponentPropsWithRef<'input'> & {
  label: string
}

const Input = forwardRef<HTMLInputElement, InputProps>(({ label, ...rest }, ref) => (
  <label>
    {label}
    <input ref={ref} {...rest} />
  </label>
))
Input.displayName = 'Input'

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

// ❌ 型エラー:string | undefined は string に代入できない
type Props = { name?: string }
const Greeting = ({ name }: Props) => <p>こんにちは、{name.toUpperCase()}</p>

// ✅ 修正:undefinedチェックを入れる
const Greeting = ({ name }: Props) => (
  <p>こんにちは、{name ? name.toUpperCase() : 'ゲスト'}</p>
)

まとめ

パターン 書き方
必須props name: string
省略可能 name?: string
デフォルト値 引数の分割代入 { name = 'default' }
children children: ReactNode
バリアント variant: 'a' | 'b' | 'c'
HTML props 継承 ComponentPropsWithoutRef<'button'>
条件付きprops Discriminated Union