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 className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
// 使用例
<Card title="カードタイトル">
<p>これは子要素です</p>
<button>ボタン</button>
</Card>実践例:複雑なProps
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
interface UserCardProps {
user: User;
onEdit?: (user: User) => void;
onDelete?: (userId: number) => void;
showActions?: boolean;
}
function UserCard({
user,
onEdit,
onDelete,
showActions = true
}: UserCardProps) {
return (
<div className="user-card">
{user.avatar && <img src={user.avatar} alt={user.name} />}
<h3>{user.name}</h3>
<p>{user.email}</p>
{showActions && (
<div>
{onEdit && <button onClick={() => onEdit(user)}>編集</button>}
{onDelete && <button onClick={() => onDelete(user.id)}>削除</button>}
</div>
)}
</div>
);
}Stateの型定義
プリミティブ型
const [count, setCount] = useState<number>(0);
const [name, setName] = useState<string>("");
const [isOpen, setIsOpen] = useState<boolean>(false);注意: 初期値から型推論できる場合、型アノテーションは省略できます。
const [count, setCount] = useState(0); // number型と推論される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 [input, setInput] = useState("");
const addTodo = () => {
const newTodo: Todo = {
id: Date.now(),
text: input,
completed: false
};
setTodos([...todos, newTodo]);
setInput("");
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={addTodo}>追加</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
</div>
);
}イベントハンドラの型
一般的なイベント型
// クリックイベント
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>
);
}カスタムフックの型定義
カスタムフックは、ロジックを再利用するための関数です。TypeScriptで型をつけることで、使う側が安全に使えます。
基本的なカスタムフック
// ローカルストレージを使うフック
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// 使用例
function App() {
const [name, setName] = useLocalStorage<string>("name", "");
const [count, setCount] = useLocalStorage<number>("count", 0);
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>増やす</button>
</div>
);
}ポイント: as const を使うと、タプル型として返せます。
データ取得フック
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
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.message}</p>;
if (!data) return <p>データがありません</p>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Contextの型定義
Contextは、コンポーネント間でデータを共有する仕組みです。TypeScriptで型をつけることで、安全に使えます。
import { createContext, useContext, useState } from 'react';
// Contextの型を定義
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => void;
logout: () => void;
}
// Contextを作成(初期値はundefined)
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Providerコンポーネント
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = (email: string, password: string) => {
// ログイン処理
setUser({ id: 1, name: "太郎", email });
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// カスタムフック
function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuthはAuthProvider内で使用してください');
}
return context;
}
// 使用例
function Profile() {
const { user, logout } = useAuth();
if (!user) {
return <p>ログインしてください</p>;
}
return (
<div>
<h1>{user.name}</h1>
<button onClick={logout}>ログアウト</button>
</div>
);
}よくあるエラーと解決法
エラー1: Property 'value' does not exist
// ❌ エラーになる
const handleChange = (e) => {
console.log(e.target.value);
};
// Error: Parameter 'e' implicitly has an 'any' type
// ✅ 型をつける
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};エラー2: Object is possibly 'null'
const [user, setUser] = useState<User | null>(null);
// ❌ エラーになる
return <h1>{user.name}</h1>;
// Error: Object is possibly 'null'
// ✅ nullチェックをする
if (!user) {
return <p>読み込み中...</p>;
}
return <h1>{user.name}</h1>;
// または Optional Chaining を使う
return <h1>{user?.name ?? "ゲスト"}</h1>;エラー3: Type 'string' is not assignable to type 'number'
const [age, setAge] = useState<number>(0);
// ❌ エラーになる
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setAge(e.target.value);
};
// Error: Type 'string' is not assignable to type 'number'
// ✅ 数値に変換する
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setAge(Number(e.target.value));
};エラー4: Argument of type is not assignable
interface ButtonProps {
onClick: () => void;
}
// ❌ エラーになる
<Button onClick={(e) => console.log(e)} />
// Error: Type '(e: any) => void' is not assignable to type '() => void'
// ✅ 型を合わせる(引数なし)
<Button onClick={() => console.log('clicked')} />
// または、Propsの型を変更
interface ButtonProps {
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
}実践的なパターン集
パターン1: 条件付きProps
// ボタンのタイプによってPropsが変わる
type ButtonProps =
| { type: 'button'; onClick: () => void }
| { type: 'link'; href: string }
| { type: 'submit' };
function Button(props: ButtonProps) {
if (props.type === 'link') {
return <a href={props.href}>リンク</a>;
}
if (props.type === 'submit') {
return <button type="submit">送信</button>;
}
return <button onClick={props.onClick}>ボタン</button>;
}
// 使用例
<Button type="button" onClick={() => alert('clicked')} />
<Button type="link" href="/about" />
<Button type="submit" />パターン2: ジェネリックなリストコンポーネント
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
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: User[] = [
{ id: 1, name: "太郎" },
{ id: 2, name: "花子" }
];
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>パターン3: フォームの型定義
// フォームの型を一元管理
interface FormFields {
email: string;
password: string;
rememberMe: boolean;
}
// フォームのエラー型
type FormErrors = {
[K in keyof FormFields]?: string;
};
function LoginForm() {
const [formData, setFormData] = useState<FormFields>({
email: "",
password: "",
rememberMe: false
});
const [errors, setErrors] = useState<FormErrors>({});
const handleChange = (field: keyof FormFields) => {
return (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.type === 'checkbox'
? e.target.checked
: e.target.value;
setFormData({ ...formData, [field]: value });
setErrors({ ...errors, [field]: undefined });
};
};
const validate = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.email) {
newErrors.email = "メールアドレスを入力してください";
}
if (!formData.password) {
newErrors.password = "パスワードを入力してください";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validate()) {
console.log('送信:', formData);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
value={formData.email}
onChange={handleChange('email')}
/>
{errors.email && <span>{errors.email}</span>}
</div>
<div>
<input
type="password"
value={formData.password}
onChange={handleChange('password')}
/>
{errors.password && <span>{errors.password}</span>}
</div>
<label>
<input
type="checkbox"
checked={formData.rememberMe}
onChange={handleChange('rememberMe')}
/>
ログイン状態を保持
</label>
<button type="submit">ログイン</button>
</form>
);
}まとめ
Propsの型
- インターフェースで型定義
- オプショナルは
?をつける - children は
React.ReactNode型
Stateの型
useState<型>(初期値)- null許容型は
型 | null - 配列は
型[]
イベントハンドラの型
React.MouseEvent<要素>:クリックイベントReact.ChangeEvent<要素>:入力変更イベントReact.FormEvent<要素>:フォーム送信イベント
カスタムフック・Context
- カスタムフックもしっかり型をつける
as constでタプル型を返せる- Contextは
createContext<型 | undefined>
エラー対処
- null チェックを忘れずに
- input.value は文字列なので Number() で変換
- エラーメッセージをよく読む
TypeScriptとReactを組み合わせることで、型安全でバグの少ないアプリケーションが作れます!