TypeScript導入
この章では、JavaScriptに型の安全性を追加してくれる「TypeScript」について一緒に学んでいきましょう。TypeScriptは最初は少し複雑に感じるかもしれませんが、慣れてくるとバグが格段に減って、より安心してコードが書けるようになりますよ。
学習目標
- TypeScriptの基本的な考え方と型システムを理解する
- JavaScriptプロジェクトにTypeScriptを導入する方法を学ぶ
- 型を使ってバグを予防する方法を覚える
- 最新のTypeScript開発環境を構築する
TypeScriptって何?
基本的な概念
TypeScriptは、Microsoftが開発したJavaScriptの「型付き版」です。普通のJavaScriptに「型」という概念を追加することで、コードをより安全に、そして書きやすくしてくれます。
TypeScriptの魅力
- 早期エラー発見: コードを書いている段階でバグを発見できる
- 開発体験の向上: 自動補完やリファクタリング機能が充実
- コードがドキュメントになる: 型が仕様書の役割を果たす
- 大規模開発に強い: チーム開発でも安心してコードが書ける
- JavaScript互換: 既存のJavaScriptコードをそのまま使える
JavaScriptとの違い
| 項目 | JavaScript | TypeScript |
|---|---|---|
| 型システム | 動的(実行時に決まる) | 静的(事前に決める) |
| エラー発見 | 実行してみないと分からない | 書いている時点で分かる |
| 開発支援 | 基本的なもの | とても充実 |
| 学習コスト | 低い | 少し高い |
| ビルド工程 | そのまま実行可能 | コンパイルが必要 |
最初は少し大変かもしれませんが、慣れてしまえばJavaScriptには戻れなくなるほど便利ですよ。
TypeScriptの基本的な書き方
1. 基本的な型
プリミティブ型
// 基本型
let message: string = "Hello TypeScript";
let count: number = 42;
let isActive: boolean = true;
let data: null = null;
let value: undefined = undefined;
// 型推論(推奨)
let name = "Alice"; // string 型として推論
let age = 30; // number 型として推論
let isStudent = false; // boolean 型として推論
配列とオブジェクト
// 配列
let numbers: number[] = [1, 2, 3, 4, 5];
let names: Array<string> = ["Alice", "Bob", "Charlie"];
// オブジェクト
let person: {
name: string;
age: number;
isStudent?: boolean; // オプショナルプロパティ
} = {
name: "Alice",
age: 30,
};
// より複雑なオブジェクト
let config: {
apiUrl: string;
timeout: number;
features: {
auth: boolean;
cache: boolean;
};
} = {
apiUrl: "https://api.example.com",
timeout: 5000,
features: {
auth: true,
cache: false,
},
};
2. インターフェースと型エイリアス
インターフェース定義
// User インターフェース
interface User {
readonly id: number; // 読み取り専用
name: string;
email: string;
age?: number; // オプショナル
}
// インターフェースの使用
const createUser = (userData: User): User => {
return {
id: Date.now(),
...userData,
};
};
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com",
};
型エイリアス
// 基本的な型エイリアス
type UserId = number;
type UserRole = "admin" | "user" | "guest";
// 複雑な型
type APIResponse<T> = {
data: T;
status: "success" | "error";
message?: string;
};
// 使用例
type UserResponse = APIResponse<User>;
const fetchUser = async (id: UserId): Promise<UserResponse> => {
// API呼び出しロジック
return {
data: { id, name: "Alice", email: "alice@example.com" },
status: "success",
};
};
3. 関数の型定義
関数シグネチャ
// 基本的な関数
function add(a: number, b: number): number {
return a + b;
}
// アロー関数
const multiply = (a: number, b: number): number => a * b;
// オプション引数とデフォルト値
const greet = (name: string, title?: string, prefix = "Mr."): string => {
return `Hello, ${title || prefix} ${name}`;
};
// 可変長引数
const sum = (...numbers: number[]): number => {
return numbers.reduce((total, num) => total + num, 0);
};
高階関数の型定義
// コールバック関数の型
type EventHandler<T> = (event: T) => void;
type Transformer<T, U> = (input: T) => U;
// 使用例
const handleClick: EventHandler<MouseEvent> = (event) => {
console.log("Clicked at:", event.clientX, event.clientY);
};
const doubleNumbers: Transformer<number[], number[]> = (numbers) => {
return numbers.map((n) => n * 2);
};
4. ジェネリクス
基本的なジェネリクス
// ジェネリック関数
function identity<T>(arg: T): T {
return arg;
}
// 使用例
const stringValue = identity("hello"); // string型
const numberValue = identity(42); // number型
// 複数の型パラメータ
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const nameAge = pair("Alice", 30); // [string, number]
ジェネリクス制約
// インターフェース制約
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // OK
logLength([1, 2, 3]); // OK
// logLength(42) // Error: number に length プロパティはない
5. ユニオン型とインターセクション型
ユニオン型(複数の型のうち一つ)
type Status = "loading" | "success" | "error";
type StringOrNumber = string | number;
// 判別可能なユニオン
interface LoadingState {
status: "loading";
}
interface SuccessState {
status: "success";
data: any;
}
interface ErrorState {
status: "error";
error: string;
}
type AppState = LoadingState | SuccessState | ErrorState;
const handleState = (state: AppState) => {
switch (state.status) {
case "loading":
console.log("Loading...");
break;
case "success":
console.log("Data:", state.data);
break;
case "error":
console.log("Error:", state.error);
break;
}
};
インターセクション型(複数の型を結合)
type Person = { name: string } & { age: number };
const person: Person = { name: "Alice", age: 30 };
プロジェクトへのTypeScript導入
1. 新規プロジェクトでのセットアップ
Vite + React + TypeScript
# プロジェクト作成
pnpm create vite my-ts-app --template react-ts
cd my-ts-app
pnpm install
# 開発サーバー起動
pnpm run dev
2. 既存JavaScriptプロジェクトの移行
# TypeScriptの追加
pnpm add -D typescript @types/node
# tsconfig.json作成
npx tsc --init
移行の流れ
.js→.tsファイル名変更- 型注釈を段階的に追加
// Before (JavaScript)
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// After (TypeScript)
interface Item {
price: number;
name: string;
}
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
3. tsconfig.json の設定
React プロジェクト向け設定
# 推奨ベース設定のインストール
pnpm add -D @tsconfig/vite-react
{
"extends": "@tsconfig/vite-react/tsconfig.json",
"compilerOptions": {
// パスエイリアス設定
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}
Note >
@tsconfig/vite-reactは Vite + React に最適化された設定を提供します。必要に応じてcompilerOptionsで上書き可能です。
React + TypeScript実践
1. コンポーネントの型定義
基本的なコンポーネント
import { ReactNode } from "react";
// Props インターフェース
interface ButtonProps {
children: ReactNode;
variant?: "primary" | "secondary";
size?: "small" | "medium" | "large";
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
// コンポーネント定義
export const Button = ({
children,
variant = "primary",
size = "medium",
disabled = false,
onClick,
}: ButtonProps) => {
return (
<button
className={`btn btn--${variant} btn--${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
// 使用例
const App = () => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("Clicked!", event.currentTarget);
};
return (
<Button variant="primary" onClick={handleClick}>
Click me
</Button>
);
};
フォームコンポーネント
import { useState, FormEvent, ChangeEvent } from "react";
interface LoginFormData {
email: string;
password: string;
}
interface LoginFormProps {
onSubmit: (data: LoginFormData) => Promise<void>;
}
export const LoginForm = ({ onSubmit }: LoginFormProps) => {
const [formData, setFormData] = useState<LoginFormData>({
email: "",
password: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSubmitting(true);
try {
await onSubmit(formData);
} catch (error) {
console.error("Login failed:", error);
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
required
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
required
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Logging in..." : "Login"}
</button>
</form>
);
};
2. カスタムHooksの型定義
useLocalStorage Hook
import { useState } from "react";
type SetValue<T> = T | ((val: T) => T);
function useLocalStorage<T>(
key: string,
initialValue: T,
): [T, (value: SetValue<T>) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: SetValue<T>) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage:`, error);
}
};
return [storedValue, setValue];
}
// 使用例
const UserSettings = () => {
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle theme: {theme}
</button>
);
};
useApi Hook
import { useState, useEffect } from "react";
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
function useApi<T>(url: string): UseApiResult<T> {
const [state, setState] = useState<{
data: T | null;
loading: boolean;
error: Error | null;
}>({ data: null, loading: true, error: null });
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true, error: null }));
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data: T = await response.json();
setState({ data, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error("Unknown error"),
});
}
};
useEffect(() => {
fetchData();
}, [url]);
return { ...state, refetch: fetchData };
}
型定義ファイルの管理
プロジェクト構造
src/
├── types/
│ ├── index.ts # 再エクスポート
│ ├── user.ts # ユーザー型
│ └── api.ts # API型
├── components/
└── hooks/
型定義の例
// src/types/user.ts
export interface User {
id: number;
name: string;
email: string;
role: "admin" | "user" | "guest";
}
export interface CreateUserRequest {
name: string;
email: string;
password: string;
}
// src/types/api.ts
export interface ApiResponse<T> {
data: T;
status: "success" | "error";
message?: string;
}
// src/types/index.ts
export * from "./user";
export * from "./api";
型ガードと型の絞り込み
型ガード関数
export function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value
);
}
// 使用例
const processUserData = (data: unknown) => {
if (isUser(data)) {
console.log(`User: ${data.name}`);
}
};
高度なTypeScript機能
ユーティリティ型
よく使う組み込み型
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Partial - すべてオプショナル
type PartialUser = Partial<User>;
// Pick - 特定のプロパティのみ
type PublicUser = Pick<User, "id" | "name" | "email">;
// Omit - 特定のプロパティを除外
type UserWithoutPassword = Omit<User, "password">;
// Required - すべて必須
type RequiredUser = Required<Partial<User>>;
条件型とマップ型
// 条件型
type NonNullable<T> = T extends null | undefined ? never : T;
// マップ型
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
トラブルシューティング
よくある問題と解決法
-
any型の乱用
// ❌ 悪い例 const fetchData = (): any => { // ... }; // ✅ 良い例 interface ApiResponse<T> { data: T; status: number; } const fetchData = <T,>(): Promise<ApiResponse<T>> => { // ... }; -
null/undefined エラー
// ❌ 危険 const getUserName = (user: User | null) => { return user.name; // user が null の可能性 }; // ✅ 安全 const getUserName = (user: User | null): string | null => { return user?.name ?? null; }; -
型アサーションの過度な使用
// ❌ 危険 const data = response as User; // ✅ 安全 const isUser = (data: unknown): data is User => { // 型ガード実装 }; if (isUser(data)) { // data は User 型として使用可能 }
まとめ
TypeScriptの利点:
- ✅ 型安全性: コンパイル時のエラー検出でバグ予防
- ✅ 開発効率: IntelliSense・リファクタリング支援
- ✅ 可読性: 型情報がドキュメントとして機能
- ✅ スケーラビリティ: 大規模開発での保守性向上
導入のベストプラクティス:
- 段階的な導入で学習コストを分散
- strict モードで厳密な型チェック
- 適切な型定義ファイル管理
- ユーティリティ型の活用でDRY原則