React & TypeScript: よりクリーンなコードのための一般的なパターン

TypeScriptを使用してReactアプリケーションを構築する際、同じ課題に繰り返し遭遇するでしょう:propsを正しく型付けする方法、イベントを安全に処理する方法、そして最大限の再利用性を持つコンポーネントを構造化する方法です。これらのパターンは単にコンパイラを満足させるためだけのものではありません。理解しやすく、リファクタリングしやすく、保守しやすいコードを書くためのものです。
この記事では、日常的に使用する最も実用的なReact TypeScriptの一般的なパターンを扱います。コンポーネントを効果的に型付けする方法、オプショナルpropsを処理する方法、イベントとrefsを扱う方法、ユーティリティ型を活用する方法、そしてreducerで判別共用体を使用する方法を学びます。各パターンには、なぜそれがコードを改善するのかを正確に示す焦点を絞った例が含まれています。
重要なポイント
- propsとstateを明示的に型付けし、インターフェースや型を使用してより良いIntelliSenseとエラー検出を実現
- オプショナルとデフォルトpropsをTypeScriptのオプショナルプロパティとデフォルトパラメータで処理
- ユーティリティ型(
Pick
やComponentProps
など)を使用して重複を減らし、一貫性を維持 - イベントとrefsを適切に型付けして型安全なDOM操作を実現
- 判別共用体をreducerで活用して包括的なアクション処理を実現
- forwardRefでコンポーネントを合成し、refsを渡す際の型安全性を維持
PropsとStateの型付け
React TypeScriptパターンの基盤は、コンポーネントのpropsを適切に型付けすることから始まります。これによりコンパイル時にエラーを捕捉し、エディタでより良い自動補完を提供します。
基本的なPropsパターン
interface ButtonProps {
label: string
onClick: () => void
disabled?: boolean
}
function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
)
}
このパターンにより、コンポーネントが正しいデータ型を受け取ることが保証されます。インターフェースはコンポーネントが期待するpropsを明確に文書化し、自己文書化されたものにします。
useStateでのState型付け
TypeScriptは通常、state型を正しく推論しますが、複雑なstateは明示的な型付けの恩恵を受けます:
import { useState } from 'react'
interface User {
id: string
name: string
email: string
}
function UserProfile() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(false) // 型はbooleanとして推論される
// TypeScriptはuserがnullの可能性があることを認識している
return user ? <div>{user.name}</div> : <div>Loading...</div>
}
オプショナルとデフォルトPropsの処理
Reactコンポーネントには、オプショナルな設定propsがよくあります。TypeScriptのオプショナルプロパティとデフォルトパラメータを組み合わせることで、クリーンなパターンが作成されます:
interface CardProps {
title: string
description?: string
variant?: 'primary' | 'secondary'
className?: string
}
function Card({
title,
description,
variant = 'primary',
className = ''
}: CardProps) {
return (
<div className={`card card-${variant} ${className}`}>
<h2>{title}</h2>
{description && <p>{description}</p>}
</div>
)
}
このパターンにより、型安全性を維持しながらコンポーネントを柔軟にします。必須propsは明確で、オプショナルpropsには適切なデフォルト値があります。
イベントとRefsの型付け
イベントハンドラーとrefsは、TypeScriptエラーの一般的な原因です。これらのパターンは推測を排除します:
イベントハンドラーパターン
import { useState, ChangeEvent, FormEvent } from 'react'
interface FormProps {
onSubmit: (data: { email: string; password: string }) => void
}
function LoginForm({ onSubmit }: FormProps) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value)
}
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
onSubmit({ email, password })
}
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={handleEmailChange} />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit">Login</button>
</form>
)
}
Refパターン
import { useRef, useEffect } from 'react'
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// TypeScriptはinputRef.currentがnullの可能性があることを認識している
inputRef.current?.focus()
}, [])
return <input ref={inputRef} type="text" />
}
ユーティリティ型の使用
TypeScriptのユーティリティ型は、コードの重複を減らし、アプリケーション全体で型の同期を保ちます。
ComponentPropsパターン
型定義を繰り返すことなく、既存のコンポーネントからpropsを抽出します:
import { ComponentProps } from 'react'
// button要素が受け入れるすべてのpropsを取得
type ButtonProps = ComponentProps<'button'> & {
variant?: 'primary' | 'secondary'
}
function StyledButton({ variant = 'primary', ...props }: ButtonProps) {
return <button className={`btn-${variant}`} {...props} />
}
// 独自のコンポーネントからpropsを抽出
type MyButtonProps = ComponentProps<typeof StyledButton>
Pickパターン
コンポーネントを拡張する際に特定のpropsを選択します:
interface UserData {
id: string
name: string
email: string
role: string
lastLogin: Date
}
// カードには一部のプロパティのみが必要
type UserCardProps = Pick<UserData, 'name' | 'email'> & {
onClick?: () => void
}
function UserCard({ name, email, onClick }: UserCardProps) {
return (
<div onClick={onClick}>
<h3>{name}</h3>
<p>{email}</p>
</div>
)
}
forwardRefでのコンポーネント合成
コンポーネントライブラリを構築する際やネイティブ要素をラップする際、forwardRef
はrefsを公開しながら型安全性を維持します:
import { forwardRef, useRef, InputHTMLAttributes } from 'react'
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string
error?: string
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
)
}
)
Input.displayName = 'Input'
// 使用時も完全な型安全性を維持
function Form() {
const inputRef = useRef<HTMLInputElement>(null)
return <Input ref={inputRef} label="Email" type="email" />
}
Reducerでの判別共用体
判別共用体により、reducerアクションが型安全になり、包括的チェックが可能になります:
interface TodoState {
items: Array<{ id: string; text: string; done: boolean }>
filter: 'all' | 'active' | 'completed'
}
type TodoAction =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: string }
| { type: 'SET_FILTER'; payload: TodoState['filter'] }
| { type: 'CLEAR_COMPLETED' }
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
items: [...state.items, {
id: Date.now().toString(),
text: action.payload,
done: false
}]
}
case 'TOGGLE_TODO':
return {
...state,
items: state.items.map(item =>
item.id === action.payload
? { ...item, done: !item.done }
: item
)
}
case 'SET_FILTER':
return { ...state, filter: action.payload }
case 'CLEAR_COMPLETED':
return {
...state,
items: state.items.filter(item => !item.done)
}
default:
// TypeScriptはすべてのケースが処理されることを保証
const exhaustive: never = action
return state
}
}
このパターンにより、すべてのアクション型を確実に処理できます。新しいアクション型を追加した場合、reducerで処理するまでTypeScriptがエラーを出します。
React 19の更新
React 19では、Actions(useActionState
、useFormStatus
)、promiseやcontextを消費するためのuse()
API、Server Componentsのファーストクラスサポートなどの新機能により、型安全性がさらに重要になります。Actionsを型付けする際は、FormData
の形状とアクションの戻り値を明示的に定義して、サーバーとクライアントのコードが同期を保つようにします。forwardRef
から標準propとしてref
を渡すことへの今後の移行も型付けを簡素化します。ref
を他のpropと同様に扱い、TypeScriptがコンポーネント境界での正確性を保証するようにします。
まとめ
これらのReact TypeScriptの一般的なパターンは、型安全なReactアプリケーションの基盤を形成します。propsとstateを明示的に型付けし、イベントとrefsを正しく処理し、TypeScriptのユーティリティ型を活用することで、より安全で保守しやすいコードを作成できます。パターンは連携して動作します。適切なprops型付けにより、より良いコンポーネント合成が可能になり、判別共用体により複雑なstate論理での包括的処理が保証されます。
基本的なprops型付けから始めて、アプリケーションが成長するにつれてユーティリティ型と高度なパターンを徐々に取り入れてください。適切な型付けへの投資は、より良いIDE支援、より少ないランタイムエラー、より明確なコンポーネント契約を通じて報われます。
よくある質問
後で拡張や宣言のマージが必要になる可能性がある場合はinterfaceを使用します。共用体、交差型、またはユーティリティ型を扱う場合はtypeを使用します。基本的なpropsではどちらでも問題ありませんので、一つのアプローチを選んで一貫性を保ってください。
最大限の柔軟性を得るためにReactNodeを使用してください:children: ReactNode。これは文字列、数値、要素、フラグメント、配列を受け入れます。より具体的なケースでは、ReactElementまたは特定の要素型を使用してください。
新しいpropsを追加しながら元のコンポーネントのprop型を保持するジェネリック関数を使用してください。ComponentPropsユーティリティ型は、重複なしに既存のコンポーネント型を抽出・拡張するのに役立ちます。
関数パラメータ、エクスポートされた関数の戻り値型、複雑なstateは明示的に型付けしてください。プリミティブでのuseStateや明らかなイベントハンドラーなどの単純なケースでは、冗長性を減らすためにTypeScriptの推論に任せてください。