デザインについての学習メモブログ

TypeScript入門 第4回:インターフェースとクラス

記事内に広告が含まれています。

TypeScript入門 第4回:インターフェースとクラス

第3回では関数とジェネリクスを学びました。

TypeScript を学ぶ初心者にとって、インターフェースとクラスは理解必須の概念です。

TypeScript入門 第4回では、TypeScript の基本から応用までをカバーし、インターフェースで型のルールを定義する方法や、クラスでオブジェクト指向設計を行う方法を解説します。

アクセス修飾子や継承、抽象クラスの使い方も具体例付きで紹介し、実践的なコード設計スキルを身につけることができます。

TypeScript を使った保守性の高いプログラミングを学びたい方におすすめの記事です。

インターフェースの詳細

インターフェースとは

インターフェースは、オブジェクトの「形」を定義する契約のようなものです。

インターフェース =「約束・ルール表」

「この機能を使うなら、こういう形のメソッドを持っていてね」という約束

約束が守られていないとエラーを出して開発段階で間違いを気づかせてくれる。

TypeScript
// 基本的なインターフェース
interface User {
  id: number;
  name: string;
  email: string;
}

// インターフェースを実装
const user: User = {
  id: 1,
  name: "太郎",
  email: "taro@example.com"
};

// プロパティが足りないとエラー
// const invalidUser: User = {
//   id: 1,
//   name: "太郎"
//   // emailが足りない!
// };

オプショナルプロパティ

TypeScript
interface Product {
  id: number;
  name: string;
  price: number;
  description?: string;      // オプショナル
  imageUrl?: string;         // オプショナル
  inStock?: boolean;         // オプショナル
}

// オプショナルプロパティは省略可能
const product1: Product = {
  id: 1,
  name: "ノートPC",
  price: 150000
};

const product2: Product = {
  id: 2,
  name: "マウス",
  price: 3000,
  description: "ワイヤレスマウス",
  inStock: true
};

readonlyプロパティ

TypeScript
interface Config {
  readonly apiKey: string;
  readonly endpoint: string;
  timeout: number;
}

const config: Config = {
  apiKey: "abc123",
  endpoint: "https://api.example.com",
  timeout: 3000
};

config.timeout = 5000;  // OK(通常のプロパティ)
// config.apiKey = "xyz";  // エラー!readonlyは変更不可

メソッドの定義

インターフェースにはメソッドも定義できます。

TypeScript
interface Calculator {
  // メソッド記法
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
  
  // プロパティ記法(関数型)
  multiply: (a: number, b: number) => number;
  divide: (a: number, b: number) => number;
}

const calculator: Calculator = {
  add(a, b) {
    return a + b;
  },
  subtract(a, b) {
    return a - b;
  },
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b
};

インターフェースの拡張

既存のインターフェースを拡張して新しいインターフェースを作れます。

TypeScript
// 基本のインターフェース
interface Person {
  name: string;
  age: number;
}

// Personを拡張
interface Employee extends Person {
  employeeId: number;
  department: string;
}

// さらに拡張
interface Manager extends Employee {
  teamSize: number;
  projects: string[];
}

const manager: Manager = {
  name: "田中",
  age: 40,
  employeeId: 123,
  department: "開発部",
  teamSize: 5,
  projects: ["プロジェクトA", "プロジェクトB"]
};

// ”複数”のインターフェースを拡張
interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface AuditInfo {
  createdBy: string;
  updatedBy: string;
}

interface Document extends Timestamped, AuditInfo {
  id: number;
  title: string;
  content: string;
}

インターフェースのマージ

同じ名前のインターフェースを複数定義すると、自動的にマージされます。

TypeScript
interface Window {
  title: string;
}

interface Window {
  width: number;
  height: number;
}

// 自動的にマージされる
const window: Window = {
  title: "マイアプリ",
  width: 800,
  height: 600
};

// この機能は主にライブラリの型定義を拡張する際に使う

クラスの基本

クラス とは「設計図」のようなイメージ

「こういうデータを持っていて、こういう操作ができるものを作る」という青写真

クラスの定義

TypeScript
class User {
  // プロパティ
  name: string;
  age: number;

  // コンストラクタ
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // メソッド
  greet(): string {
    return `こんにちは、${this.name}です。`;
  }

  isAdult(): boolean {
    return this.age >= 20;
  }
}

// インスタンスの作成
const user = new User("太郎", 25);
console.log(user.greet());      // "こんにちは、太郎です。"
console.log(user.isAdult());    // true

アクセス修飾子

クラスのメンバーへのアクセスを制御できます。

TypeScript
class BankAccount {
  // public:どこからでもアクセス可能(デフォルト)
  public accountNumber: string;
  
  // private:クラス内部からのみアクセス可能
  private balance: number;
  
  // protected:クラスとサブクラスからアクセス可能
  protected accountType: string;

  constructor(accountNumber: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.balance = initialBalance;
    this.accountType = "普通";
  }

  // public メソッド
  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
    }
  }

  public getBalance(): number {
    return this.balance;
  }

  // private メソッド
  private validateAmount(amount: number): boolean {
    return amount > 0 && amount <= this.balance;
  }

  public withdraw(amount: number): boolean {
    if (this.validateAmount(amount)) {
      this.balance -= amount;
      return true;
    }
    return false;
  }
}

const account = new BankAccount("123-456", 10000);
account.deposit(5000);
console.log(account.getBalance());  // 15000

// account.balance = 1000000;  // エラー!privateにアクセス不可

コンストラクタの省略記法

パラメータプロパティを使うと、コードを簡潔に書けます。

TypeScript
// 通常の書き方
class User1 {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

// 省略記法(アクセス修飾子を付ける)
class User2 {
  constructor(
    public name: string,
    public age: number,
    private email: string
  ) {}
  // プロパティの宣言と初期化が自動的に行われる
}

const user = new User2("太郎", 25, "taro@example.com");
console.log(user.name);   // "太郎"
console.log(user.age);    // 25
// console.log(user.email);  // エラー!privateにアクセス不可

Getter と Setter

TypeScript
class Temperature {
  private _celsius: number = 0;

  // getter
  get celsius(): number {
    return this._celsius;
  }

  // setter
  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("絶対零度以下の温度は設定できません");
    }
    this._celsius = value;
  }

  // 華氏温度を計算するgetter
  get fahrenheit(): number {
    return this._celsius * 9/5 + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5/9; //this.celsiusに"_"が無いのは10行目のセッターを呼び出している
  }
}

const temp = new Temperature();
temp.celsius = 25;
console.log(temp.celsius);     // 25
console.log(temp.fahrenheit);  // 77

temp.fahrenheit = 86;
console.log(temp.celsius);     // 30

静的メンバー

クラス自体に属するメンバーを定義できます。

TypeScript
class MathUtil {
  // 静的プロパティ
  static PI: number = 3.14159;
  
  // 静的メソッド
  static square(x: number): number {
    return x * x;
  }

  static cube(x: number): number {
    return x * x * x;
  }

  // インスタンスメソッド
  randomInt(max: number): number {
    return Math.floor(Math.random() * max);
  }
}

// 静的メンバーはクラス名でアクセス
console.log(MathUtil.PI);           // 3.14159
console.log(MathUtil.square(5));    // 25

// インスタンスメソッドはインスタンス経由でアクセス
const util = new MathUtil();
console.log(util.randomInt(100));

// 実用例:シングルトンパターン
class Database {
  private static instance: Database;
  
  private constructor() {
    // privateコンストラクタでインスタンス化を制限
  }

  static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }

  query(sql: string): void {
    console.log(`Executing: ${sql}`);
  }
}

// const db = new Database();  // エラー!コンストラクタがprivate
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2);  // true(同じインスタンス)

シングルトンパターンの使い所「アプリ全体で1つだけ存在し、共有されるべき責務」を安全に管理したいときに使います。

シングルトンが向いているケース

  • グローバルに共有する設定・状態管理
    アプリ設定(Config)
    環境変数のラッパー
  • ログ・監視・メトリクス系
    Logger
    Analytics
    ErrorReporter
  • リソースコストが高いもの
    DBコネクション
    キャッシュ(Redis)
    APIクライアント
  • アプリ全体の調整役(Coordinator)
    イベントバス
    ストア(簡易的な状態管理)
    プラグイン管理

インターフェースとクラスの組み合わせ

クラスでインターフェースを実装

TypeScript
// インターフェースで契約を定義
interface Animal {
  name: string;
  makeSound(): string;
}

// クラスでインターフェースを実装
class Dog implements Animal {
  constructor(public name: string) {}

  makeSound(): string {
    return "ワンワン";
  }

  // クラス独自のメソッド
  fetch(): string {
    return `${this.name}がボールを取ってきた`;
  }
}

class Cat implements Animal {
  constructor(public name: string) {}

  makeSound(): string {
    return "ニャー";
  }

  scratch(): string {
    return `${this.name}が爪を研いでいる`;
  }
}

// インターフェース型として扱える
const animals: Animal[] = [
  new Dog("ポチ"),
  new Cat("タマ")
];

animals.forEach(animal => {
  console.log(`${animal.name}: ${animal.makeSound()}`);
});

複数のインターフェースを実装

TypeScript
interface Flyable {
  fly(): void;
  altitude: number;
}

interface Swimmable {
  swim(): void;
  depth: number;
}

// 複数のインターフェースを実装
class Duck implements Flyable, Swimmable {
  altitude: number = 0;
  depth: number = 0;

  constructor(public name: string) {}

  fly(): void {
    this.altitude = 100;
    console.log(`${this.name}が空を飛んでいる`);
  }

  swim(): void {
    this.depth = 2;
    console.log(`${this.name}が泳いでいる`);
  }
}

const duck = new Duck("ドナルド");
duck.fly();
duck.swim();

継承

基本的な継承

TypeScript
// 基底クラス
class Animal {
  constructor(public name: string) {}

  move(distance: number): void {
    console.log(`${this.name}${distance}m移動した`);
  }

  makeSound(): string {
    return "何か音を出す";
  }
}

// 派生クラス
class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);  // 親クラスのコンストラクタを呼び出す
  }

  // メソッドのオーバーライド
  makeSound(): string {
    return "ワンワン";
  }

  // 独自のメソッド
  fetch(): void {
    console.log(`${this.name}がボールを取ってくる`);
  }
}

class Bird extends Animal {
  constructor(name: string, public wingspan: number) {
    super(name);
  }

  makeSound(): string {
    return "ピヨピヨ";
  }

  fly(): void {
    console.log(`${this.name}が飛んでいる`);
  }
}

const dog = new Dog("ポチ", "柴犬");
dog.move(10);           // "ポチが10m移動した"
console.log(dog.makeSound());  // "ワンワン"
dog.fetch();            // "ポチがボールを取ってくる"

const bird = new Bird("ピーちゃん", 30);
bird.move(5);           // "ピーちゃんが5m移動した"
bird.fly();             // "ピーちゃんが飛んでいる"

protectedの活用

TypeScript
class Employee {
  protected employeeId: number;
  
  constructor(
    public name: string,
    employeeId: number
  ) {
    this.employeeId = employeeId;
  }

  protected getEmployeeInfo(): string {
    return `ID: ${this.employeeId}, 名前: ${this.name}`;
  }
}

class Manager extends Employee {
  constructor(
    name: string,
    employeeId: number,
    public department: string
  ) {
    super(name, employeeId);
  }

  getInfo(): string {
    // protectedメンバーにアクセス可能
    return `${this.getEmployeeInfo()}, 部署: ${this.department}`;
  }

  showEmployeeId(): void {
    console.log(this.employeeId);  // OK
  }
}

const manager = new Manager("田中", 123, "開発部");
console.log(manager.getInfo());
// manager.employeeId;  // エラー!外部からはアクセス不可

抽象クラス

インスタンス化できないクラスで、継承して使います。

TypeScript
// 抽象クラス
abstract class Shape {
  constructor(public color: string) {}

  // 抽象メソッド(実装は派生クラスで)
  abstract getArea(): number;
  abstract getPerimeter(): number;

  // 通常のメソッド
  describe(): string {
    return `これは${this.color}の図形で、面積は${this.getArea()}です`;
  }
}

class Circle extends Shape {
  constructor(color: string, public radius: number) {
    super(color);
  }

  getArea(): number {
    return Math.PI * this.radius ** 2;
  }

  getPerimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(
    color: string,
    public width: number,
    public height: number
  ) {
    super(color);
  }

  getArea(): number {
    return this.width * this.height;
  }

  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }
}

// const shape = new Shape("赤");  // エラー!抽象クラスはインスタンス化不可

const circle = new Circle("青", 5);
console.log(circle.describe());

const rect = new Rectangle("緑", 4, 6);
console.log(rect.describe());

実践例:Todoアプリケーション

学んだ知識を使って、実践的なTodoアプリを作ってみましょう。

TypeScript
// Todoの状態
type TodoStatus = "pending" | "in_progress" | "completed";

// Todoインターフェース
interface ITodo {
  id: number;
  title: string;
  description: string;
  status: TodoStatus;
  createdAt: Date;
  updatedAt: Date;
}

// Todoクラス
class Todo implements ITodo {
  public updatedAt: Date;

  constructor(
    public id: number,
    public title: string,
    public description: string,
    public status: TodoStatus = "pending",
    public createdAt: Date = new Date()
  ) {
    this.updatedAt = new Date();
  }

  // ステータスを変更
  updateStatus(newStatus: TodoStatus): void {
    this.status = newStatus;
    this.updatedAt = new Date();
  }

  // 完了にする
  complete(): void {
    this.updateStatus("completed");
  }

  // 進行中にする
  start(): void {
    this.updateStatus("in_progress");
  }

  // 情報を表示
  display(): string {
    return `[${this.status}] ${this.title} - ${this.description}`;
  }
}

// Todo管理クラス
class TodoManager {
  private todos: Todo[] = [];
  private nextId: number = 1;

  // Todoを追加
  addTodo(title: string, description: string): Todo {
    const todo = new Todo(this.nextId++, title, description);
    this.todos.push(todo);
    return todo;
  }

  // IDでTodoを取得
  getTodo(id: number): Todo | undefined {
    return this.todos.find(todo => todo.id === id);
  }

  // 全てのTodoを取得
  getAllTodos(): Todo[] {
    return [...this.todos];  // コピーを返す
  }

  // ステータスでフィルタ
  getTodosByStatus(status: TodoStatus): Todo[] {
    return this.todos.filter(todo => todo.status === status);
  }

  // Todoを削除
  deleteTodo(id: number): boolean {
    const index = this.todos.findIndex(todo => todo.id === id);
    if (index !== -1) {
      this.todos.splice(index, 1);
      return true;
    }
    return false;
  }

  // 統計情報
  getStats(): { total: number; completed: number; pending: number } {
    return {
      total: this.todos.length,
      completed: this.getTodosByStatus("completed").length,
      pending: this.getTodosByStatus("pending").length
    };
  }
}

// 使用例
const manager = new TodoManager();

// Todoを追加
const todo1 = manager.addTodo("TypeScriptを学ぶ", "第4回まで読む");
const todo2 = manager.addTodo("記事を書く", "ブログ記事を執筆");
const todo3 = manager.addTodo("買い物", "スーパーで食材を買う");

// ステータスを変更
todo1.complete();
todo2.start();

// 表示
manager.getAllTodos().forEach(todo => {
  console.log(todo.display());
});

// 統計
console.log(manager.getStats());
// { total: 3, completed: 1, pending: 1 }

実践例:ユーザー認証システム

TypeScript
// ユーザーの役割
enum UserRole {
  Admin = "admin",
  User = "user",
  Guest = "guest"
}

// 認証可能なインターフェース
interface IAuthenticatable {
  authenticate(password: string): boolean;
}

// 権限チェック可能なインターフェース
interface IAuthorizable {
  hasPermission(action: string): boolean;
}

// 基本のユーザークラス
abstract class BaseUser implements IAuthenticatable {
  protected passwordHash: string;

  constructor(
    public id: number,
    public username: string,
    public email: string,
    password: string
  ) {
    this.passwordHash = this.hashPassword(password);
  }

  private hashPassword(password: string): string {
    // 実際にはbcryptなどを使用
    return `hashed_${password}`;
  }

  authenticate(password: string): boolean {
    return this.passwordHash === this.hashPassword(password);
  }

  abstract getRole(): UserRole;
}

// 管理者クラス
class Admin extends BaseUser implements IAuthorizable {
  constructor(id: number, username: string, email: string, password: string) {
    super(id, username, email, password);
  }

  getRole(): UserRole {
    return UserRole.Admin;
  }

  hasPermission(action: string): boolean {
    // 管理者は全ての権限を持つ
    return true;
  }

  // 管理者専用メソッド
  deleteUser(userId: number): void {
    console.log(`ユーザー ${userId} を削除しました`);
  }
}

// 一般ユーザークラス
class RegularUser extends BaseUser implements IAuthorizable {
  private permissions: Set<string> = new Set(["read", "write"]);

  constructor(id: number, username: string, email: string, password: string) {
    super(id, username, email, password);
  }

  getRole(): UserRole {
    return UserRole.User;
  }

  hasPermission(action: string): boolean {
    return this.permissions.has(action);
  }

  addPermission(permission: string): void {
    this.permissions.add(permission);
  }
}

// 認証マネージャー
class AuthManager {
  private users: Map<number, BaseUser> = new Map();

  registerUser(user: BaseUser): void {
    this.users.set(user.id, user);
  }

  login(username: string, password: string): BaseUser | null {
    for (const user of this.users.values()) {
      if (user.username === username && user.authenticate(password)) {
        return user;
      }
    }
    return null;
  }

  checkPermission(user: BaseUser, action: string): boolean {
    if ("hasPermission" in user) {
      return (user as IAuthorizable).hasPermission(action);
    }
    return false;
  }
}

// 使用例
const authManager = new AuthManager();

const admin = new Admin(1, "admin", "admin@example.com", "admin123");
const user = new RegularUser(2, "taro", "taro@example.com", "password123");

authManager.registerUser(admin);
authManager.registerUser(user);

// ログイン
const loggedInUser = authManager.login("taro", "password123");
if (loggedInUser) {
  console.log(`${loggedInUser.username}がログインしました`);
  console.log(`役割: ${loggedInUser.getRole()}`);
  
  // 権限チェック
  console.log("読み取り権限:", authManager.checkPermission(loggedInUser, "read"));
  console.log("削除権限:", authManager.checkPermission(loggedInUser, "delete"));
}

デザインパターンの例

ファクトリーパターン

TypeScript
interface Product {
  getName(): string;
  getPrice(): number;
}

class Laptop implements Product {
  constructor(private model: string, private price: number) {}

  getName(): string {
    return `ノートPC ${this.model}`;
  }

  getPrice(): number {
    return this.price;
  }
}

class Smartphone implements Product {
  constructor(private model: string, private price: number) {}

  getName(): string {
    return `スマートフォン ${this.model}`;
  }

  getPrice(): number {
    return this.price;
  }
}

// ファクトリークラス
class ProductFactory {
  static createProduct(type: "laptop" | "smartphone", model: string, price: number): Product {
    switch (type) {
      case "laptop":
        return new Laptop(model, price);
      case "smartphone":
        return new Smartphone(model, price);
      default:
        throw new Error("Unknown product type");
    }
  }
}

// 使用例
const laptop = ProductFactory.createProduct("laptop", "ThinkPad", 150000);
const phone = ProductFactory.createProduct("smartphone", "iPhone", 100000);

console.log(laptop.getName(), laptop.getPrice());
console.log(phone.getName(), phone.getPrice());

ファクトリーパターンは一言でいうと「作り方を利用者から隠す仕組み」
関心は「生成ロジックの分離」

ファクトリーパターンを使うと

  • 作り方が1か所に集約
  • 使う側は new を知らない
  • 修正点が少なくなる

JS/TS では 関数で十分です。必ずクラスで作る必要は無いです。

オブザーバーパターン

TypeScript
// 観察者のインターフェース
interface Observer {
  update(data: any): void;
}

// 観察対象のクラス
class Subject {
  private observers: Observer[] = [];

  attach(observer: Observer): void {
    this.observers.push(observer);
  }

  detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(data: any): void {
    this.observers.forEach(observer => observer.update(data));
  }
}

// 具体的な観察者
class EmailNotifier implements Observer {
  update(data: any): void {
    console.log(`メール送信: ${JSON.stringify(data)}`);
  }
}

class SMSNotifier implements Observer {
  update(data: any): void {
    console.log(`SMS送信: ${JSON.stringify(data)}`);
  }
}

class Logger implements Observer {
  update(data: any): void {
    console.log(`ログ記録: ${JSON.stringify(data)}`);
  }
}

// 使用例
const subject = new Subject();

const emailNotifier = new EmailNotifier();
const smsNotifier = new SMSNotifier();
const logger = new Logger();

subject.attach(emailNotifier);
subject.attach(smsNotifier);
subject.attach(logger);

subject.notify({ event: "新規ユーザー登録", userId: 123 });

オブザーバーパターン =「変化があったら、登録者全員に自動で知らせる仕組み」

知りたい人だけが登録して、何か起きたら、まとめて通知される

本質は「呼び出す側」と「呼ばれる側」を切り離す

YouTubeを例にすると
新動画を出すと 登録者全員に通知が飛ぶ。登録・解除は自由。チャンネル主は「誰が見ているか」を細かく知らなくていい。

まとめ

  • 「変化を知らせる」ためのパターン
  • 登録 → 通知 → 解除
  • 複数の処理を安全にぶら下げられる
  • イベント駆動の基本形

まとめ

今回は、インターフェースとクラスについて学びました。

学んだこと

  • インターフェースの定義と拡張
  • クラスの基本とアクセス修飾子
  • インターフェースの実装
  • 継承と抽象クラス
  • 実践的なクラス設計
  • デザインパターンの基礎

次回予告 第5回では、実際のプロジェクトでTypeScriptを活用する方法、設定ファイルの詳細、実践的な開発フローについて学びます。

TypeScript入門シリーズ

第1回: 環境構築から全体像まで
第2回: 型の基礎を深く理解する
第3回: 関数と型、ジェネリクス入門
第4回: インターフェースとクラス
第5回: 実践編 – 実際のプロジェクトでの活用