← ホームに戻る

TypeScript + React - 実践的な型定義

ReactコンポーネントでTypeScriptを使いこなそう

TypeScript + React - 実践的な型定義

ReactとTypeScriptを組み合わせると、より安全で保守しやすいコードが書けます。


Propsの型定義

基本形

interface ButtonProps { label: string; onClick: () => void; } function Button({ label, onClick }: ButtonProps) { return <button onClick={onClick}>{label}</button>; } // 使用例 <Button label="クリック" onClick={() => alert('クリックされました')} />

オプショナルなProps

interface ButtonProps { label: string; onClick: () => void; disabled?: boolean; // オプショナル variant?: 'primary' | 'secondary'; // オプショナル } function Button({ label, onClick, disabled = false, // デフォルト値 variant = 'primary' // デフォルト値 }: ButtonProps) { return ( <button onClick={onClick} disabled={disabled} className={variant} > {label} </button> ); }

childrenの型

interface CardProps { title: string; children: React.ReactNode; // 任意のReact要素 } function Card({ title, children }: CardProps) { return ( <div> <h2>{title}</h2> <div>{children}</div> </div> ); } // 使用例 <Card title="カードタイトル"> <p>これは子要素です</p> <button>ボタン</button> </Card>

Stateの型定義

プリミティブ型

const [count, setCount] = useState<number>(0); const [name, setName] = useState<string>(""); const [isOpen, setIsOpen] = useState<boolean>(false);

注意: 初期値から型推論できる場合、型アノテーションは省略できます。

const [count, setCount] = useState(0); // number型と推論される

オブジェクト型

interface User { id: number; name: string; email: string; } function UserProfile() { const [user, setUser] = useState<User>({ id: 0, name: "", email: "" }); const updateName = (name: string) => { setUser({ ...user, name }); }; return ( <div> <input value={user.name} onChange={(e) => updateName(e.target.value)} /> </div> ); }

null許容型

interface User { id: number; name: string; } function UserProfile() { const [user, setUser] = useState<User | null>(null); useEffect(() => { // APIからユーザーデータを取得 fetch('/api/user') .then(res => res.json()) .then(data => setUser(data)); }, []); if (!user) { return <p>読み込み中...</p>; } return ( <div> <h1>{user.name}</h1> </div> ); }

配列型

interface Todo { id: number; text: string; completed: boolean; } function TodoList() { const [todos, setTodos] = useState<Todo[]>([]); const addTodo = (text: string) => { const newTodo: Todo = { id: Date.now(), text, completed: false }; setTodos([...todos, newTodo]); }; const toggleTodo = (id: number) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); }; return ( <ul> {todos.map(todo => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> {todo.text} </li> ))} </ul> ); }

イベントハンドラの型

一般的なイベント型

// クリックイベント const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { console.log('クリックされました'); }; // 入力イベント const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { console.log(e.target.value); }; // フォーム送信イベント const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); console.log('送信されました'); }; // キーボードイベント const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter') { console.log('Enterが押されました'); } };

実践例:フォーム

interface FormData { email: string; password: string; } function LoginForm() { const [formData, setFormData] = useState<FormData>({ email: "", password: "" }); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); }; const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); console.log('ログイン:', formData); }; return ( <form onSubmit={handleSubmit}> <input name="email" type="email" value={formData.email} onChange={handleChange} /> <input name="password" type="password" value={formData.password} onChange={handleChange} /> <button type="submit">ログイン</button> </form> ); }

よく使う型定義パターン

ユニオン型(複数の型のどれか)

type Status = 'loading' | 'success' | 'error'; interface ApiState { status: Status; data: any | null; error: string | null; } function DataFetcher() { const [state, setState] = useState<ApiState>({ status: 'loading', data: null, error: null }); // ... }

判別可能なユニオン型

type LoadingState = { status: 'loading'; }; type SuccessState = { status: 'success'; data: any; }; type ErrorState = { status: 'error'; error: string; }; type State = LoadingState | SuccessState | ErrorState; function Component() { const [state, setState] = useState<State>({ status: 'loading' }); // 型ガードで安全に扱える if (state.status === 'loading') { return <p>読み込み中...</p>; } if (state.status === 'error') { return <p>エラー: {state.error}</p>; } return <div>{state.data}</div>; }

ジェネリックコンポーネント

interface ListProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; } function List<T>({ items, renderItem }: ListProps<T>) { return ( <ul> {items.map((item, index) => ( <li key={index}>{renderItem(item)}</li> ))} </ul> ); } // 使用例 interface User { id: number; name: string; } const users: User[] = [ { id: 1, name: "太郎" }, { id: 2, name: "花子" } ]; <List items={users} renderItem={(user) => <span>{user.name}</span>} />

カスタムフックの型定義

// カウンターフック function useCounter(initialValue: number = 0) { const [count, setCount] = useState(initialValue); const increment = () => setCount(c => c + 1); const decrement = () => setCount(c => c - 1); const reset = () => setCount(initialValue); return { count, increment, decrement, reset }; } // 使用例 function Counter() { const { count, increment, decrement, reset } = useCounter(0); return ( <div> <p>{count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>リセット</button> </div> ); }

型を返すカスタムフック

interface UseFetchResult<T> { data: T | null; loading: boolean; error: string | null; } function useFetch<T>(url: string): UseFetchResult<T> { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { fetch(url) .then(res => res.json()) .then(data => { setData(data); setLoading(false); }) .catch(err => { setError(err.message); setLoading(false); }); }, [url]); return { data, loading, error }; } // 使用例 interface User { id: number; name: string; } function UserList() { const { data, loading, error } = useFetch<User[]>('/api/users'); if (loading) return <p>読み込み中...</p>; if (error) return <p>エラー: {error}</p>; return ( <ul> {data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }

まとめ

Props

  • インターフェースで型定義
  • オプショナルは ? をつける
  • children は React.ReactNode

State

  • useState<型>(初期値)
  • null許容型は 型 | null
  • 配列は 型[]

イベントハンドラ

  • React.MouseEvent<要素>
  • React.ChangeEvent<要素>
  • React.FormEvent<要素>

パターン

  • ユニオン型で状態を表現
  • ジェネリックで再利用可能に
  • カスタムフックで型を返す

TypeScriptとReactを組み合わせることで、型安全で保守しやすいアプリケーションが作れます!