TypeScript入門 - 型安全なJavaScript開発

入門 | 50分 で読める | 2024.12.16

このチュートリアルで学ぶこと

✓ TypeScriptの基本的な型
✓ 型推論の仕組み
✓ インターフェースと型エイリアス
✓ ユニオン型とリテラル型
✓ ジェネリクスの基本
✓ 実践的な型定義パターン

前提条件

  • JavaScriptの基本知識
  • Node.jsがインストールされていること

プロジェクトのセットアップ

# プロジェクトディレクトリ作成
mkdir typescript-tutorial
cd typescript-tutorial

# package.json作成
npm init -y

# TypeScriptインストール
npm install -D typescript ts-node @types/node

# tsconfig.json作成
npx tsc --init

tsconfig.jsonの設定

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

Step 1: 基本的な型

プリミティブ型

// src/01-primitives.ts

// 文字列
let name: string = "太郎";
let greeting: string = `こんにちは、${name}さん`;

// 数値
let age: number = 25;
let price: number = 1980.5;
let hex: number = 0xff;

// 真偽値
let isActive: boolean = true;
let hasPermission: boolean = false;

// null と undefined
let nothing: null = null;
let notDefined: undefined = undefined;

// any(型チェックを無効化 - できるだけ避ける)
let anything: any = "string";
anything = 123;
anything = true;

// unknown(anyより安全)
let unknownValue: unknown = "hello";
// unknownValue.toUpperCase(); // エラー
if (typeof unknownValue === "string") {
  unknownValue.toUpperCase(); // OK
}

配列とタプル

// src/02-arrays.ts

// 配列
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];

// ジェネリック記法
let values: Array<number> = [10, 20, 30];

// タプル(固定長・固定型の配列)
let tuple: [string, number] = ["年齢", 25];
let rgb: [number, number, number] = [255, 128, 0];

// タプルのラベル付き
let user: [name: string, age: number] = ["太郎", 30];

// 読み取り専用配列
let readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4); // エラー

オブジェクト型

// src/03-objects.ts

// オブジェクト型の定義
let person: { name: string; age: number } = {
  name: "花子",
  age: 28
};

// オプショナルプロパティ
let config: { host: string; port?: number } = {
  host: "localhost"
  // portは省略可能
};

// 読み取り専用プロパティ
let point: { readonly x: number; readonly y: number } = {
  x: 10,
  y: 20
};
// point.x = 30; // エラー

// インデックスシグネチャ
let dictionary: { [key: string]: string } = {
  hello: "こんにちは",
  goodbye: "さようなら"
};
dictionary["thanks"] = "ありがとう"; // OK

Step 2: 関数の型

基本的な関数型

// src/04-functions.ts

// パラメータと戻り値の型
function add(a: number, b: number): number {
  return a + b;
}

// アロー関数
const multiply = (a: number, b: number): number => a * b;

// オプショナルパラメータ
function greet(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}!`;
}

// デフォルトパラメータ
function createUser(
  name: string,
  age: number = 20,
  role: string = "user"
): { name: string; age: number; role: string } {
  return { name, age, role };
}

// 残余パラメータ
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, curr) => acc + curr, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // 15

関数型の定義

// 関数型の変数
let calculator: (a: number, b: number) => number;
calculator = (x, y) => x + y;

// 型エイリアスで関数型を定義
type MathOperation = (a: number, b: number) => number;

const subtract: MathOperation = (a, b) => a - b;
const divide: MathOperation = (a, b) => a / b;

// コールバック関数
function processArray(
  arr: number[],
  callback: (item: number) => number
): number[] {
  return arr.map(callback);
}

const doubled = processArray([1, 2, 3], (x) => x * 2);
console.log(doubled); // [2, 4, 6]

void と never

// void - 戻り値がない
function logMessage(message: string): void {
  console.log(message);
}

// never - 決して戻らない
function throwError(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

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

型エイリアス(type)

// src/05-type-alias.ts

// 基本的な型エイリアス
type ID = string | number;
type Point = { x: number; y: number };

// ユニオン型
type Status = "pending" | "approved" | "rejected";
type Result = string | null;

let userId: ID = "user_123";
let orderId: ID = 456;
let currentStatus: Status = "pending";

// 交差型
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;

const person: Person = {
  name: "太郎",
  age: 30
};

インターフェース

// src/06-interface.ts

// 基本的なインターフェース
interface User {
  id: number;
  name: string;
  email: string;
  age?: number; // オプショナル
  readonly createdAt: Date; // 読み取り専用
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
  createdAt: new Date()
};

// インターフェースの継承
interface Employee extends User {
  department: string;
  salary: number;
}

const employee: Employee = {
  id: 2,
  name: "Bob",
  email: "bob@example.com",
  createdAt: new Date(),
  department: "Engineering",
  salary: 500000
};

// メソッドを持つインターフェース
interface Calculator {
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
}

const calc: Calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};

typeとinterfaceの違い

// type: ユニオン、プリミティブ、タプルに使用可能
type StringOrNumber = string | number;
type Tuple = [string, number];

// interface: 拡張が可能(同名で宣言すると自動マージ)
interface Window {
  title: string;
}
interface Window {
  size: { width: number; height: number };
}
// → Windowは両方のプロパティを持つ

// 一般的な使い分け
// - オブジェクトの形状定義 → interface
// - ユニオン型、プリミティブ → type
// - 関数型 → type

Step 4: ユニオン型とリテラル型

ユニオン型

// src/07-union.ts

// 基本的なユニオン型
type StringOrNumber = string | number;

function printId(id: StringOrNumber) {
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    console.log(id.toFixed(2));
  }
}

// 配列のユニオン型
type MixedArray = (string | number)[];
const mixed: MixedArray = [1, "two", 3, "four"];

リテラル型

// 文字列リテラル型
type Direction = "north" | "south" | "east" | "west";

function move(direction: Direction) {
  console.log(`Moving ${direction}`);
}

move("north"); // OK
// move("up"); // エラー

// 数値リテラル型
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceValue {
  return Math.ceil(Math.random() * 6) as DiceValue;
}

// ブールリテラル型
type True = true;

判別可能なユニオン型

// src/08-discriminated-union.ts

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

type Shape = Circle | Square | Rectangle;

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}

const circle: Circle = { kind: "circle", radius: 5 };
console.log(calculateArea(circle)); // 78.54...

Step 5: ジェネリクス

基本的なジェネリクス

// src/09-generics.ts

// ジェネリック関数
function identity<T>(value: T): T {
  return value;
}

const str = identity<string>("hello");
const num = identity(42); // 型推論でnumber

// 配列を扱うジェネリック関数
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const firstNumber = first([1, 2, 3]); // number | undefined
const firstString = first(["a", "b", "c"]); // string | undefined

ジェネリックインターフェース

// APIレスポンスの型
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

interface User {
  id: number;
  name: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "Alice" },
  status: 200,
  message: "Success"
};

const productResponse: ApiResponse<Product[]> = {
  data: [
    { id: 1, title: "Item 1", price: 1000 },
    { id: 2, title: "Item 2", price: 2000 }
  ],
  status: 200,
  message: "Success"
};

制約付きジェネリクス

// extends で型制約を追加
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(value: T): void {
  console.log(value.length);
}

logLength("hello"); // OK: stringはlengthを持つ
logLength([1, 2, 3]); // OK: 配列はlengthを持つ
// logLength(123); // エラー: numberはlengthを持たない

// keyof を使った制約
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: "太郎", age: 30 };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
// getProperty(person, "email"); // エラー

Step 6: ユーティリティ型

組み込みユーティリティ型

// src/10-utility-types.ts

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial - すべてオプショナルに
type PartialUser = Partial<User>;
const updateData: PartialUser = { name: "新しい名前" };

// Required - すべて必須に
type RequiredUser = Required<User>;

// Pick - 特定のプロパティのみ選択
type UserBasic = Pick<User, "id" | "name">;
const basic: UserBasic = { id: 1, name: "Alice" };

// Omit - 特定のプロパティを除外
type UserWithoutEmail = Omit<User, "email">;

// Readonly - すべて読み取り専用に
type ReadonlyUser = Readonly<User>;

// Record - キーと値の型を指定したオブジェクト
type UserRoles = Record<string, "admin" | "user" | "guest">;
const roles: UserRoles = {
  alice: "admin",
  bob: "user"
};

実践的な使用例

// フォームの状態管理
interface FormData {
  username: string;
  email: string;
  password: string;
}

// フォームのエラー状態
type FormErrors = Partial<Record<keyof FormData, string>>;

const errors: FormErrors = {
  email: "有効なメールアドレスを入力してください"
};

// APIリクエスト/レスポンス
type CreateUserRequest = Omit<User, "id">;
type UpdateUserRequest = Partial<Omit<User, "id">>;
type UserResponse = Readonly<User>;

function createUser(data: CreateUserRequest): UserResponse {
  return { id: Date.now(), ...data } as UserResponse;
}

実践課題: 型安全なTodoアプリ

// src/todo-app.ts

// 型定義
type TodoStatus = "pending" | "in_progress" | "completed";
type Priority = "low" | "medium" | "high";

interface Todo {
  id: number;
  title: string;
  description?: string;
  status: TodoStatus;
  priority: Priority;
  createdAt: Date;
  completedAt?: Date;
}

type CreateTodoInput = Omit<Todo, "id" | "createdAt" | "completedAt">;
type UpdateTodoInput = Partial<Omit<Todo, "id" | "createdAt">>;

// Todoリストクラス
class TodoList {
  private todos: Todo[] = [];
  private nextId = 1;

  add(input: CreateTodoInput): Todo {
    const todo: Todo = {
      ...input,
      id: this.nextId++,
      createdAt: new Date()
    };
    this.todos.push(todo);
    return todo;
  }

  update(id: number, input: UpdateTodoInput): Todo | null {
    const index = this.todos.findIndex(t => t.id === id);
    if (index === -1) return null;

    const updated = { ...this.todos[index], ...input };
    if (input.status === "completed" && !updated.completedAt) {
      updated.completedAt = new Date();
    }
    this.todos[index] = updated;
    return updated;
  }

  delete(id: number): boolean {
    const index = this.todos.findIndex(t => t.id === id);
    if (index === -1) return false;
    this.todos.splice(index, 1);
    return true;
  }

  getAll(): Readonly<Todo[]> {
    return this.todos;
  }

  getByStatus(status: TodoStatus): Todo[] {
    return this.todos.filter(t => t.status === status);
  }

  getByPriority(priority: Priority): Todo[] {
    return this.todos.filter(t => t.priority === priority);
  }
}

// 使用例
const todoList = new TodoList();

todoList.add({
  title: "TypeScriptを学ぶ",
  description: "基本的な型システムを理解する",
  status: "in_progress",
  priority: "high"
});

todoList.add({
  title: "Reactを学ぶ",
  status: "pending",
  priority: "medium"
});

console.log(todoList.getAll());
console.log(todoList.getByStatus("pending"));

ベストプラクティス

1. 型推論を活用
   - 明示的な型注釈は必要な場所のみ
   - 関数の戻り値は注釈を付けると良い

2. anyを避ける
   - unknownを使って型チェックを行う
   - 型が分からない場合はジェネリクスを検討

3. strictモードを有効化
   - tsconfig.jsonで"strict": true
   - より安全なコードが書ける

4. 適切な型定義を選択
   - オブジェクト → interface
   - ユニオン、タプル → type

5. ユーティリティ型を活用
   - 既存の型から新しい型を導出
   - 重複を避けて保守性を向上

まとめ

TypeScriptの型システムを活用することで、コンパイル時にエラーを検出し、より安全なコードを書けます。基本の型から始めて、ジェネリクスやユーティリティ型を使いこなせるようになりましょう。

← 一覧に戻る