はじめに
これまでの勉強会では、ReactのコードをJavaScriptで書いてきました。しかし、実際のプロジェクトではTypeScriptを使うことが一般的になっています。
TypeScriptを使うことで:
- バグを早期に発見できる(コンパイル時にエラーを検出)
- コードの意図が明確になる(型がドキュメントの役割)
- リファクタリングが安全になる(影響範囲を把握できる)
- IDEの補完が強力になる(自動補完、型情報の表示)
今回は、TypeScriptの基本から、Reactでの型定義、型ドリブン開発の実践まで学んでいきます。
なぜTypeScriptを使うのか
JavaScriptとTypeScriptの違いを見てみましょう。
// JavaScript - 型がない
function greet(name) {
return `Hello, ${name}!`;
}
greet('Alice'); // OK
greet(123); // 実行時まで問題に気づかない
greet(); // 実行時まで問題に気づかない
greet(null); // 実行時エラーの可能性// TypeScript - 型がある
function greet(name: string): string {
return `Hello, ${name}!`;
}
greet('Alice'); // OK
greet(123); // ❌ コンパイルエラー
greet(); // ❌ コンパイルエラー
greet(null); // ❌ コンパイルエラー型があることで、実行前にエラーを発見できます。
Reactコンポーネントでの例
// JavaScript - propsの型が不明
function UserCard({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.emal}</p> {/* タイポ!でも気づかない */}
</div>
);
}// TypeScript - propsの型を定義
interface UserCardProps {
user: {
name: string;
email: string;
age: number;
};
}
function UserCard({ user }: UserCardProps) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.emal}</p> {/* ❌ コンパイルエラー: 'emal'は存在しない */}
</div>
);
}TypeScriptの基本的な型
プリミティブ型
// 基本的な型
const name: string = 'Alice';
const age: number = 25;
const isActive: boolean = true;
const data: null = null;
const value: undefined = undefined;
// 配列
const numbers: number[] = [1, 2, 3];
const names: string[] = ['Alice', 'Bob'];
const mixed: (string | number)[] = ['Alice', 25]; // Union型オブジェクト型
// オブジェクトの型定義
interface User {
id: number;
name: string;
email: string;
age?: number; // オプショナル(?を付ける)
readonly role: string; // 読み取り専用
}
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
role: 'admin'
};
user.name = 'Bob'; // OK
user.role = 'user'; // ❌ エラー: readonlyは変更できない関数の型
// 関数の引数と戻り値の型
function add(a: number, b: number): number {
return a + b;
}
// アロー関数
const multiply = (a: number, b: number): number => {
return a * b;
};
// 戻り値がない関数
function log(message: string): void {
console.log(message);
}
// オプショナル引数
function greet(name: string, greeting?: string): string {
return `${greeting || 'Hello'}, ${name}!`;
}Union型とIntersection型
// Union型: AまたはB
type Status = 'success' | 'error' | 'loading';
function handleStatus(status: Status) {
if (status === 'success') {
console.log('成功');
}
// status === 'pending' // ❌ エラー: 'pending'は存在しない
}
// Intersection型: AかつB
interface Person {
name: string;
age: number;
}
interface Employee {
employeeId: string;
department: string;
}
type EmployeePerson = Person & Employee;
const employee: EmployeePerson = {
name: 'Alice',
age: 25,
employeeId: 'E001',
department: 'Engineering'
};Reactコンポーネントの型定義
関数コンポーネントの型定義
// 方法1: Propsの型を定義
interface ButtonProps {
children: React.ReactNode;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
function Button({ children, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{children}
</button>
);
}
// 方法2: React.FCを使う(現在は非推奨)
const Button: React.FC<ButtonProps> = ({ children, onClick, variant = 'primary', disabled }) => {
return (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
);
};イベントハンドラの型
function Form() {
// インプットのイベント
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// テキストエリアのイベント
const handleTextAreaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
console.log(e.target.value);
};
// フォーム送信
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('送信');
};
// クリックイベント
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log('クリック');
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<textarea onChange={handleTextAreaChange} />
<button onClick={handleClick}>送信</button>
</form>
);
}Hooksの型定義
// useState
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);
// useRef
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);
// useContext
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useThemeはThemeProvider内で使用してください');
}
return context;
}childrenの型
// ReactNodeを使うのが最も柔軟
interface LayoutProps {
children: React.ReactNode;
}
// 特定の要素だけを受け入れる
interface ListProps {
children: React.ReactElement<ItemProps> | React.ReactElement<ItemProps>[];
}
// JSX要素のみ
interface ContainerProps {
children: JSX.Element;
}
// 複数の子要素
interface WrapperProps {
children: React.ReactNode[];
}型推論:TypeScriptに型を推測させる
TypeScriptは多くの場合、型を自動的に推測できます。
// 型推論: TypeScriptが自動的に型を推測
const name = 'Alice'; // string型と推測
const age = 25; // number型と推測
const isActive = true; // boolean型と推測
// 配列の型推論
const numbers = [1, 2, 3]; // number[]型と推測
const mixed = [1, 'two', 3]; // (string | number)[]型と推測
// オブジェクトの型推論
const user = {
name: 'Alice',
age: 25
}; // { name: string; age: number; }型と推測
// 関数の戻り値の型推論
function add(a: number, b: number) {
return a + b; // number型と推測
}
// Reactコンポーネントでの型推論
function Counter() {
const [count, setCount] = useState(0); // number型と推測
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>増やす</button>
</div>
);
}型推論を活用することで、コードがシンプルになります。ただし、明示的な型定義が必要な場合もあります。
// 型推論では不十分な例
const [user, setUser] = useState(null); // null型と推測されてしまう
// 明示的に型を指定
const [user, setUser] = useState<User | null>(null); // User | null型型ガード:型を絞り込む
型ガードを使うと、条件分岐で型を絞り込めます。
typeof型ガード
function processValue(value: string | number) {
if (typeof value === 'string') {
// この中では value は string型
console.log(value.toUpperCase());
} else {
// この中では value は number型
console.log(value.toFixed(2));
}
}instanceof型ガード
class Dog {
bark() {
console.log('ワンワン');
}
}
class Cat {
meow() {
console.log('ニャー');
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // Dogのメソッドが使える
} else {
animal.meow(); // Catのメソッドが使える
}
}in型ガード
interface Bird {
fly: () => void;
layEggs: () => void;
}
interface Fish {
swim: () => void;
layEggs: () => void;
}
function move(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly(); // Birdのメソッドが使える
} else {
animal.swim(); // Fishのメソッドが使える
}
}ユーザー定義型ガード
interface User {
id: number;
name: string;
email: string;
}
interface Admin {
id: number;
name: string;
email: string;
role: 'admin';
}
// 型ガード関数
function isAdmin(user: User | Admin): user is Admin {
return 'role' in user && user.role === 'admin';
}
function greetUser(user: User | Admin) {
if (isAdmin(user)) {
// この中では user は Admin型
console.log(`管理者: ${user.name}, 権限: ${user.role}`);
} else {
// この中では user は User型
console.log(`ユーザー: ${user.name}`);
}
}Reactでの型ガードの活用
interface SuccessState {
status: 'success';
data: string[];
}
interface ErrorState {
status: 'error';
error: string;
}
interface LoadingState {
status: 'loading';
}
type ApiState = SuccessState | ErrorState | LoadingState;
function ApiComponent({ state }: { state: ApiState }) {
if (state.status === 'loading') {
return <div>読み込み中...</div>;
}
if (state.status === 'error') {
// この中では state.error にアクセス可能
return <div>エラー: {state.error}</div>;
}
// この中では state.data にアクセス可能
return (
<ul>
{state.data.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
}ジェネリクス:型を抽象化する
ジェネリクスを使うと、再利用可能な型安全なコードが書けます。
基本的なジェネリクス
// ジェネリクスなし: 型ごとに関数を書く必要がある
function getFirstNumber(arr: number[]): number {
return arr[0];
}
function getFirstString(arr: string[]): string {
return arr[0];
}
// ジェネリクスあり: 1つの関数で全ての型に対応
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const firstNumber = getFirst([1, 2, 3]); // number型
const firstName = getFirst(['a', 'b', 'c']); // string型
const firstBool = getFirst([true, false]); // boolean型Reactコンポーネントでのジェネリクス
// 汎用的なリストコンポーネント
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;
}
function UserList() {
const users: User[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
return (
<List
items={users}
renderItem={(user) => (
<div>
{user.id}: {user.name}
</div>
)}
/>
);
}カスタムフックでのジェネリクス
// 汎用的なデータ取得フック
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then((data: T) => {
setData(data);
setLoading(false);
})
.catch((err: Error) => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// 使用例
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error.message}</div>;
if (!user) return <div>ユーザーが見つかりません</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}ジェネリクスの制約
// lengthプロパティを持つ型のみ受け入れる
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength('hello'); // OK: stringはlengthを持つ
getLength([1, 2, 3]); // OK: 配列はlengthを持つ
getLength({ length: 10 }); // OK: lengthプロパティがある
getLength(123); // ❌ エラー: numberはlengthを持たないUtility Types:便利な型変換
TypeScriptには組み込みのUtility Typesがあります。
Partial - 全てのプロパティをオプショナルに
interface User {
id: number;
name: string;
email: string;
age: number;
}
// 一部のプロパティだけを更新する関数
function updateUser(id: number, updates: Partial<User>) {
// updatesは全てのプロパティがオプショナル
console.log(`ユーザー${id}を更新:`, updates);
}
updateUser(1, { name: 'Alice' }); // OK
updateUser(2, { email: 'bob@example.com' }); // OK
updateUser(3, { name: 'Charlie', age: 30 }); // OKRequired - 全てのプロパティを必須に
interface Config {
host?: string;
port?: number;
protocol?: 'http' | 'https';
}
// 全てのプロパティを必須にする
type RequiredConfig = Required<Config>;
const config: RequiredConfig = {
host: 'localhost',
port: 3000,
protocol: 'http'
// どれか1つでも欠けているとエラー
};Readonly - 全てのプロパティを読み取り専用に
interface User {
id: number;
name: string;
}
const user: Readonly<User> = {
id: 1,
name: 'Alice'
};
user.name = 'Bob'; // ❌ エラー: 読み取り専用Pick - 特定のプロパティだけを抽出
interface User {
id: number;
name: string;
email: string;
age: number;
address: string;
}
// nameとemailだけを抽出
type UserPreview = Pick<User, 'name' | 'email'>;
const preview: UserPreview = {
name: 'Alice',
email: 'alice@example.com'
// id, age, addressは不要
};Omit - 特定のプロパティを除外
interface User {
id: number;
name: string;
email: string;
password: string;
}
// passwordを除外
type UserWithoutPassword = Omit<User, 'password'>;
const user: UserWithoutPassword = {
id: 1,
name: 'Alice',
email: 'alice@example.com'
// passwordは含まれない
};Record - キーと値の型を指定
// キーがstring、値がnumberのオブジェクト
type Scores = Record<string, number>;
const scores: Scores = {
math: 90,
english: 85,
science: 92
};
// より厳密な型定義
type Status = 'pending' | 'approved' | 'rejected';
type StatusCounts = Record<Status, number>;
const counts: StatusCounts = {
pending: 5,
approved: 10,
rejected: 2
};型ドリブン開発の実践
型を先に定義してから実装する開発手法です。
1. まず型を定義する
// APIのレスポンス型を定義
interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
interface ApiResponse<T> {
data: T;
meta: {
total: number;
page: number;
perPage: number;
};
}
// コンポーネントのProps型を定義
interface UserListProps {
onUserSelect: (user: User) => void;
filter?: 'active' | 'inactive';
}2. 型に沿って実装する
// 型定義に従って実装
function UserList({ onUserSelect, filter }: UserListProps) {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users?filter=${filter || 'active'}`)
.then(res => res.json())
.then((response: ApiResponse<User[]>) => {
setUsers(response.data);
setLoading(false);
});
}, [filter]);
if (loading) return <div>読み込み中...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserSelect(user)}>
{user.name}
</li>
))}
</ul>
);
}3. 型のメリット
// ✅ IDEの補完が効く
function handleUser(user: User) {
console.log(user.); // name, email, id, createdAtが候補に出る
}
// ✅ タイポをコンパイル時に検出
function printUser(user: User) {
console.log(user.emal); // ❌ エラー: 'emal'は存在しない
}
// ✅ リファクタリングが安全
// Userのプロパティを変更すると、使用箇所全てでエラーが出る
interface User {
id: number;
name: string;
email: string;
// createdAtを削除
}
function formatDate(user: User) {
return user.createdAt; // ❌ エラー: 'createdAt'は存在しない
}実践例:型安全なフォーム
// フォームの値の型
interface FormData {
name: string;
email: string;
age: number;
terms: boolean;
}
// バリデーションエラーの型
type FormErrors = Partial<Record<keyof FormData, string>>;
function RegistrationForm() {
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
age: 0,
terms: false
});
const [errors, setErrors] = useState<FormErrors>({});
// 型安全な更新関数
const updateField = <K extends keyof FormData>(
field: K,
value: FormData[K]
) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const validate = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.name) {
newErrors.name = '名前は必須です';
}
if (!formData.email.includes('@')) {
newErrors.email = '有効なメールアドレスを入力してください';
}
if (formData.age < 18) {
newErrors.age = '18歳以上である必要があります';
}
if (!formData.terms) {
newErrors.terms = '利用規約に同意してください';
}
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="text"
value={formData.name}
onChange={(e) => updateField('name', e.target.value)}
/>
{errors.name && <span>{errors.name}</span>}
</div>
<div>
<input
type="email"
value={formData.email}
onChange={(e) => updateField('email', e.target.value)}
/>
{errors.email && <span>{errors.email}</span>}
</div>
<div>
<input
type="number"
value={formData.age}
onChange={(e) => updateField('age', Number(e.target.value))}
/>
{errors.age && <span>{errors.age}</span>}
</div>
<div>
<input
type="checkbox"
checked={formData.terms}
onChange={(e) => updateField('terms', e.target.checked)}
/>
<label>利用規約に同意する</label>
{errors.terms && <span>{errors.terms}</span>}
</div>
<button type="submit">登録</button>
</form>
);
}よくあるTypeScriptのアンチパターン
1. any型の乱用
// ❌ 悪い例: any型を使う
function processData(data: any) {
return data.value.toUpperCase(); // ランタイムエラーの可能性
}
// ✅ 良い例: 適切な型を定義
interface Data {
value: string;
}
function processData(data: Data) {
return data.value.toUpperCase(); // 型安全
}2. 型アサーションの乱用
// ❌ 悪い例: 型アサーションで強制的に型を変える
const user = {} as User; // 空のオブジェクトをUserとして扱う(危険)
// ✅ 良い例: 適切に値を設定する
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com'
};3. 型定義の重複
// ❌ 悪い例: 同じような型を何度も定義
interface UserResponse {
id: number;
name: string;
email: string;
}
interface UserData {
id: number;
name: string;
email: string;
}
// ✅ 良い例: 1つの型を再利用
interface User {
id: number;
name: string;
email: string;
}
type UserResponse = User;
type UserData = User;まとめ
TypeScriptで型安全なReactアプリケーションを構築するポイント:
基本
- プリミティブ型: string, number, boolean
- オブジェクト型: interface, type
- Union型: A | B(AまたはB)
- Intersection型: A & B(AかつB)
React固有の型
- コンポーネントProps: interfaceで定義
- イベントハンドラ: React.ChangeEvent, React.MouseEvent
- Hooks:
useState<T>,useRef<T> - children: React.ReactNode
高度な型機能
- 型推論: TypeScriptに型を推測させる
- 型ガード: typeof, instanceof, in, ユーザー定義
- ジェネリクス: 再利用可能な型安全なコード
- Utility Types: Partial, Required, Pick, Omit, Record
型ドリブン開発
- まず型を定義する
- 型に沿って実装する
- コンパイルエラーで安全性を確保
TypeScriptを使いこなすことで、バグの少ない保守性の高いReactアプリケーションを構築できます!
次のステップ: ここまでで基礎的な内容は一通り学びました。次は以下のような応用的なトピックに挑戦してみましょう:
- パフォーマンス最適化(useMemo、useCallback、React 19の新機能)
- 実践的なアプリケーション構築(状態管理ライブラリ、ルーティング)
- テスト(Jest、React Testing Library)
- アーキテクチャ編(実際にコードを書いた経験を積んでから読むと理解が深まります)
参考リンク
The starting point for learning TypeScriptFind TypeScript starter projects: from Angular to React or Node.js and CLIs.
www.typescriptlang.org
React TypeScript Cheatsheets | React TypeScript CheatsheetsReact TypeScript Cheatsheets
react-typescript-cheatsheet.netlify.app

