第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 |