Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

第4章: TypeScript入門

学習目標

この章では、TypeScriptの基礎を学び、型安全なコンポーネントを作成します。

  • なぜ型が必要なのかを理解する
  • 型システムがどのようにバグを防ぐかを理解する
  • TypeScriptの基本概念
  • 型定義の方法
  • インターフェースと型エイリアス
  • ジェネリクス
  • Next.jsでのTypeScript活用

成果物: 型安全なコンポーネント


0. なぜTypeScriptが必要なのか

0-1. JavaScriptの問題点

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("太郎", "山田"); // "こんにちは、太郎 山田さん" ← 順番が逆!
// しかも実行してみるまでわからない

これらの問題は、コードが実行されるまで気づけません。

0-2. TypeScriptの解決策

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("太郎", "山田"); // ❌ エラー!

0-3. 型システムがバグを防ぐメカニズム

コンパイル時チェック:

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 が表示される
// タイポを防ぎ、開発速度が上がる

0-4. 実際のメリット

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;
}

// チーム全員がこの形を守る
// バックエンドとフロントエンドで認識のズレが起きにくい

0-5. TypeScriptのコンパイルプロセス

TypeScriptコード (.ts / .tsx)
  ↓
TypeScript コンパイラ (tsc)
  ↓
型チェック
  ├─ エラーあり → コンパイルエラー表示
  └─ エラーなし → 次へ
  ↓
JavaScriptコード (.js)
  ↓
ブラウザで実行

重要なポイント: ブラウザはJavaScriptしか理解できません。TypeScriptは開発時のツールであり、最終的にはJavaScriptに変換されます。


1. TypeScriptとは

1-0. TypeScriptの歴史

  • 2012年: Microsoft が開発・公開
  • 目的: 大規模なJavaScriptアプリケーションの開発を改善
  • 現在: React、Vue、Angularなど、多くのフレームワークが標準採用

TypeScriptは、JavaScriptに「型」を追加した言語です。

メリット:

  • バグを事前に発見できる(型エラーがコンパイル時に検出される)
  • エディタの補完が強力になる(型情報でインテリセンス)
  • リファクタリングが安全(型で追跡できる)
  • コードの意図が明確になる(型=ドキュメント)

2. 基本的な型

2-1. プリミティブ型

// 文字列
const name: string = "太郎";

// 数値
const age: number = 25;

// 真偽値
const isActive: boolean = true;

// null と undefined
const empty: null = null;
const notDefined: undefined = undefined;

// 型推論(自動で型を判定)
const message = "こんにちは"; // string型と推論される

2-2. 配列とオブジェクト

// 配列
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
};

3. インターフェースと型エイリアス

3-1. インターフェース

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()
};

3-2. 型エイリアス

type Status = "active" | "inactive" | "pending"; // ユニオン型

type User = {
  id: number;
  name: string;
  status: Status;
};

const user: User = {
  id: 1,
  name: "太郎",
  status: "active"
};

3-3. インターフェース vs 型エイリアス

// インターフェース: 拡張できる
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// 型エイリアス: ユニオン型やタプルに便利
type ID = string | number;
type Point = [number, number];

4. 関数の型

4-1. 基本的な関数の型

// 引数と戻り値の型
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型と推論される

4-2. オプション引数とデフォルト引数

function greet(name: string, greeting?: string): string {
  return `${greeting || "こんにちは"}${name}さん`;
}

function createUser(name: string, age: number = 20): User {
  return { name, age };
}

5. ジェネリクス

型を抽象化して再利用可能にします。

// ジェネリック関数
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型

6. Reactコンポーネントの型

6-1. Props の型定義

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>
  );
}

6-2. State の型

'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);

  // ...
}

7. 実践: 型安全なToDoアプリ

types/todo.ts

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
  createdAt: Date;
}

export type TodoInput = Omit<Todo, 'id' | 'createdAt'>;

export type FilterType = 'all' | 'active' | 'completed';

components/TodoItem.tsx

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>
  );
}

app/page.tsx

'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>
  );
}

ミニ課題

課題1: ユーザー管理機能を追加

ToDoに「担当者」フィールドを追加してください。

解答例
// types/todo.ts
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
  assignee?: string; // 追加
  createdAt: Date;
}

よくあるエラーと解決策

エラー1: Type 'string' is not assignable to type 'number'

原因: 型が一致しない

解決策: 型変換を行う

const age: number = parseInt(ageString);

エラー2: Object is possibly 'undefined'

原因: 値が 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の使い方を理解できる

React との統合

  • Props の型を定義できる
  • State の型を定義できる
  • イベントハンドラの型を理解している

次のステップへの準備

  • Reactで型安全なコンポーネントを書く準備ができている

参考リンク