この章では、TypeScriptの基礎を学び、型安全なコンポーネントを作成します。
- なぜ型が必要なのかを理解する
- 型システムがどのようにバグを防ぐかを理解する
- TypeScriptの基本概念
- 型定義の方法
- インターフェースと型エイリアス
- ジェネリクス
- Next.jsでのTypeScript活用
成果物: 型安全なコンポーネント
JavaScriptは「動的型付け言語」です。これは柔軟性がある一方で、問題も引き起こします。
問題例1: 気づきにくいバグ
// JavaScript
function add(a, b) {
return a + b;
}
console.log(add(1, 2)); // 3 ← OK
console.log(add("1", "2")); // "12" ← 文字列結合!
console.log(add(1, "2")); // "12" ← 意図しない動作問題例2: 存在しないプロパティへのアクセス
const user = {
name: "太郎",
age: 25
};
console.log(user.email); // undefined ← エラーにならない!
// 後でこれが原因でバグが発生する可能性問題例3: 関数の引数の間違い
function greet(firstName, lastName) {
return `こんにちは、${firstName} ${lastName}さん`;
}
// 引数を逆に渡してもエラーにならない
greet("太郎", "山田"); // "こんにちは、太郎 山田さん" ← 順番が逆!
// しかも実行してみるまでわからないこれらの問題は、コードが実行されるまで気づけません。
TypeScriptは「静的型付け言語」です。コードを書いている時点で型をチェックします。
解決策1: 型を明示する
// TypeScript
function add(a: number, b: number): number {
return a + b;
}
console.log(add(1, 2)); // 3 ← OK
console.log(add("1", "2")); // ❌ コンパイルエラー!
console.log(add(1, "2")); // ❌ コンパイルエラー!VSCodeが即座に赤線を引いて教えてくれます。実行する前にバグが見つかります。
解決策2: オブジェクトの型を定義する
interface User {
name: string;
age: number;
}
const user: User = {
name: "太郎",
age: 25
};
console.log(user.email); // ❌ コンパイルエラー!
// "Property 'email' does not exist on type 'User'"解決策3: 関数の型を明確にする
interface Person {
firstName: string;
lastName: string;
}
function greet(person: Person): string {
return `こんにちは、${person.firstName} ${person.lastName}さん`;
}
// 引数がPersonオブジェクトである必要がある
greet({ firstName: "太郎", lastName: "山田" }); // OK
greet("太郎", "山田"); // ❌ エラー!コンパイル時チェック:
TypeScriptは、JavaScriptに変換(コンパイル)される前に型をチェックします。
あなたがコードを書く
↓
TypeScript コンパイラがチェック
↓
型エラーがある?
YES → ❌ コンパイルエラー表示(実行前に発見)
NO → ✅ JavaScriptに変換
↓
ブラウザで実行
型推論:
型を明示的に書かなくても、TypeScriptが自動で推論します。
// 型を明示しなくても
const message = "こんにちは"; // TypeScriptが string 型と推論
const count = 10; // TypeScriptが number 型と推論
// 後でこう書くとエラー
message = 123; // ❌ エラー: number型をstring型に代入できないエディタの補完が強力になる理由:
interface User {
id: number;
name: string;
email: string;
age: number;
}
const user: User = { ... };
// user. と打つと、VSCodeが自動で補完候補を表示
// id, name, email, age が表示される
// タイポを防ぎ、開発速度が上がる1. バグの早期発見:
従来: コードを書く → 実行 → バグ発見 → 修正 → 実行 → ...
TypeScript: コードを書く → 即座にエラー表示 → 修正 → 完成
2. リファクタリングが安全:
// JavaScriptで関数名を変更
function calculateTotal() { ... }
// この関数を使っている箇所が100個あったら?
// 手作業で100箇所変更?見落としがあったら?// TypeScriptなら
function calculateTotal() { ... }
// 関数名を変更すると、使っている箇所すべてにエラーが表示される
// 見落としがない!3. ドキュメントとしての役割:
// 型定義を見れば、何を渡せば良いかすぐわかる
function createUser(
name: string,
age: number,
email: string
): User {
// ...
}
// 引数の順番や型が明確4. チーム開発での意思疎通:
// インターフェースでAPIの形を定義
interface ApiResponse {
success: boolean;
data: User[];
errorMessage?: string;
}
// チーム全員がこの形を守る
// バックエンドとフロントエンドで認識のズレが起きにくいTypeScriptコード (.ts / .tsx)
↓
TypeScript コンパイラ (tsc)
↓
型チェック
├─ エラーあり → コンパイルエラー表示
└─ エラーなし → 次へ
↓
JavaScriptコード (.js)
↓
ブラウザで実行
重要なポイント: ブラウザはJavaScriptしか理解できません。TypeScriptは開発時のツールであり、最終的にはJavaScriptに変換されます。
- 2012年: Microsoft が開発・公開
- 目的: 大規模なJavaScriptアプリケーションの開発を改善
- 現在: React、Vue、Angularなど、多くのフレームワークが標準採用
TypeScriptは、JavaScriptに「型」を追加した言語です。
メリット:
- バグを事前に発見できる(型エラーがコンパイル時に検出される)
- エディタの補完が強力になる(型情報でインテリセンス)
- リファクタリングが安全(型で追跡できる)
- コードの意図が明確になる(型=ドキュメント)
// 文字列
const name: string = "太郎";
// 数値
const age: number = 25;
// 真偽値
const isActive: boolean = true;
// null と undefined
const empty: null = null;
const notDefined: undefined = undefined;
// 型推論(自動で型を判定)
const message = "こんにちは"; // string型と推論される// 配列
const numbers: number[] = [1, 2, 3];
const fruits: Array<string> = ["apple", "banana"];
// タプル(固定長配列)
const tuple: [string, number] = ["太郎", 25];
// オブジェクト
const user: {
name: string;
age: number;
email?: string; // ? は省略可能
} = {
name: "太郎",
age: 25
};interface User {
id: number;
name: string;
email: string;
age?: number; // 省略可能
readonly createdAt: Date; // 読み取り専用
}
const user: User = {
id: 1,
name: "太郎",
email: "taro@example.com",
createdAt: new Date()
};type Status = "active" | "inactive" | "pending"; // ユニオン型
type User = {
id: number;
name: string;
status: Status;
};
const user: User = {
id: 1,
name: "太郎",
status: "active"
};// インターフェース: 拡張できる
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 型エイリアス: ユニオン型やタプルに便利
type ID = string | number;
type Point = [number, number];// 引数と戻り値の型
function add(a: number, b: number): number {
return a + b;
}
// アロー関数
const multiply = (a: number, b: number): number => {
return a * b;
};
// 型推論(戻り値の型は省略可能)
const subtract = (a: number, b: number) => a - b; // number型と推論されるfunction greet(name: string, greeting?: string): string {
return `${greeting || "こんにちは"}、${name}さん`;
}
function createUser(name: string, age: number = 20): User {
return { name, age };
}型を抽象化して再利用可能にします。
// ジェネリック関数
function identity<T>(value: T): T {
return value;
}
const num = identity<number>(42);
const str = identity<string>("hello");
// 配列のジェネリクス
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const firstNumber = getFirstElement([1, 2, 3]); // number型
const firstString = getFirstElement(["a", "b"]); // string型interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
disabled?: boolean;
}
export function Button({ label, onClick, variant = "primary", disabled = false }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`px-4 py-2 rounded ${
variant === "primary" ? "bg-blue-500" : "bg-gray-500"
}`}
>
{label}
</button>
);
}'use client'
import { useState } from 'react';
interface User {
id: number;
name: string;
email: string;
}
export default function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(false);
// ...
}export interface Todo {
id: number;
title: string;
completed: boolean;
createdAt: Date;
}
export type TodoInput = Omit<Todo, 'id' | 'createdAt'>;
export type FilterType = 'all' | 'active' | 'completed';import { Todo } from '@/types/todo';
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
return (
<div className="flex items-center gap-3 p-3 border rounded">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span className={todo.completed ? 'line-through text-gray-500' : ''}>
{todo.title}
</span>
<button
onClick={() => onDelete(todo.id)}
className="ml-auto text-red-500"
>
削除
</button>
</div>
);
}'use client'
import { useState } from 'react';
import { Todo, FilterType } from '@/types/todo';
import { TodoItem } from '@/components/TodoItem';
export default function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState('');
const [filter, setFilter] = useState<FilterType>('all');
const addTodo = () => {
if (!input.trim()) return;
const newTodo: Todo = {
id: Date.now(),
title: input,
completed: false,
createdAt: new Date()
};
setTodos([...todos, newTodo]);
setInput('');
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<div className="min-h-screen bg-gray-100 py-8 px-4">
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
<h1 className="text-3xl font-bold mb-6">ToDoアプリ</h1>
<div className="flex gap-2 mb-6">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
className="flex-1 px-4 py-2 border rounded"
placeholder="新しいタスクを入力"
/>
<button
onClick={addTodo}
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
追加
</button>
</div>
<div className="flex gap-2 mb-4">
{(['all', 'active', 'completed'] as FilterType[]).map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-4 py-2 rounded ${
filter === f ? 'bg-blue-500 text-white' : 'bg-gray-200'
}`}
>
{f === 'all' ? 'すべて' : f === 'active' ? '未完了' : '完了'}
</button>
))}
</div>
<div className="space-y-2">
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</div>
</div>
</div>
);
}ToDoに「担当者」フィールドを追加してください。
解答例
// types/todo.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
assignee?: string; // 追加
createdAt: Date;
}原因: 型が一致しない
解決策: 型変換を行う
const age: number = parseInt(ageString);原因: 値が undefined の可能性がある
解決策: オプショナルチェーンを使用
const name = user?.name;この章では以下のことを学びました:
- ✅ TypeScriptの基本的な型
- ✅ インターフェースと型エイリアス
- ✅ ジェネリクス
- ✅ Reactコンポーネントの型定義
- ✅ 型安全なToDoアプリの作成
次の章では、Reactの基礎をさらに深く学びます。
学習を完了したら、以下の項目をチェックしてください:
- 静的型付けと動的型付けの違いを理解している
- TypeScriptがJavaScriptのスーパーセットであることを理解している
- コンパイル時の型チェックの利点を理解している
- TypeScriptの型推論の仕組みを理解している
- プリミティブ型(string, number, boolean)を使える
- 配列の型(string[], number[])を定義できる
- オブジェクトの型を定義できる
- union型(string | number)を使える
- optional型(?)を使える
- interfaceで型を定義できる
- type で型エイリアスを作成できる
- interface と type の違いを理解している
- 引数の型を指定できる
- 戻り値の型を指定できる
- voidとundefinedの違いを理解している
- TypeScriptファイル(.ts)を作成できる
- 型エラーを読んで修正できる
-
npm run buildでコンパイルできる - 型定義を見てAPIの使い方を理解できる
- Props の型を定義できる
- State の型を定義できる
- イベントハンドラの型を理解している
- Reactで型安全なコンポーネントを書く準備ができている