1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptの練習帳!~Class設計編~ (レベル別の練習アリ)

Last updated at Posted at 2024-06-01

TypeScriptとClass

この記事は今のフロントエンドでは欠かせない存在となってきているTypeScriptを触り始めた方に向けた内容となっています。

もはやReact/Next.jsでもTypeScriptで記載することが多くなってきているので、一気に理解を深めていきましょう。

また、こちらのチュートリアルが一番おすすめなので、もし基本がすでに理解できていると言う方はこちらをお勧めします。

この記事は上記のチュートリアルがまだちょっとわからない!といった方向けに、JavaScriptの上位互換とも言われているTypeScriptでClass設計をする事で、なんとなく型付のコツみたいなものがわかるようになるので、まずはコードリーディングして意味を理解してからハンズオンで実施していただきたいなと思います。

※JSでClassが理解できていることが前提ですので、もし不安な方がいれば復習お願いします

こちらのサイトも参考になるので目を通しておくといいと思います。

まず理解しておくべきこと

Classの設計に入る前に最低限理解しておくべきことを簡単にですが確認しましょう。

アクセス修飾子

クラスのメンバー(プロパティやメソッド)に対してアクセス修飾子を使用することができます。主なアクセス修飾子はpublicprivate、そしてprotectedです。

これらの修飾子はメンバーの可視性とアクセス可能性を制御します。

  • public: どこからでもアクセス可能です。デフォルトの修飾子です。

  • protected: そのクラスおよび継承したサブクラスからアクセス可能です。

  • private: そのクラス内からのみアクセス可能です。

  • static: インスタンスごとに存在せず、インスタンスを介さずに直接アクセスできます。
    逆にインスタンスからはアクセスできません。

  • ちなみにprivateprotectedインスタンス化したオブジェクトからはアクセス不可です。

SetterとGetter

Setter

  • 検証と制御値が設定される前に検証や加工を行います。 例えば、数値が特定の範囲内にあるか、文字列が特定の形式を満たしているかなどをチェックします。
  • データの整合性の保持:不適切な値が設定されるのを防ぎ、クラスの状態を正しい形で保持します。

Getter

  • データの取得:setterの制御をクリアした値を取得
  • データのフォーマット:必要に応じて、値を特定の形式に整形してから返すことができます。

省略できる記法

以下の2つは同じ内容です

class Person {
  public name: string;
  private age: number;
  protected email: string;

  constructor(name: string, age: number, email: string) {
    this.name = name;
    this.age = age;
    this.email = email;
  }
class Person {
  constructor(public name: string, private age: number, protected email: string) {
    // ここをここを空にする事で name, age, email は自動的にクラスのプロパティになります
  }

さて、ここからはレベル別Classの設計練習をしていきます。
確認内容を踏まえた上でレベル別のクラス設計をしていきましょう!

初級編

Exercise"ユーザー情報取得"

クラスの概要

Person クラス

  • 概要: 個人の名前、年齢、メールアドレスを管理するクラス。
  • プロパティ:
    • name(パブリック): 名前。
    • age(プライベート): 年齢。
    • email(プロテクテッド): メールアドレス。
  • メソッド:
    • constructor(name: string, age: number, email: string): 名前、年齢、メールアドレスを設定。
    • greet(): 挨拶メッセージを表示。
    • getAge()(プライベート): 年齢を取得。
    • getEmail()(プロテクテッド): メールアドレスを取得。

Employee クラス

  • 概要: Person クラスを継承し、追加で部署を管理するクラス。
  • プロパティ:
    • department(パブリック): 部署。
  • メソッド:
    • constructor(name: string, age: number, email: string, department: string): 名前、年齢、メールアドレス、部署を設定。
    • getEmployeeInfo(): 従業員の名前、部署、メールアドレスを表示。
    class Person {
      constructor(public name: string, private age: number, protected email: string) {
        // ここで name, age, email は自動的にクラスのプロパティになります
      }
    
      public greet() {
        console.log(`こんにちは、私の名前は${this.name}です。`);
      }
    
      private getAge() {
        return this.age;
      }
    
      protected getEmail() {
        return this.email;
      }
    }
    
    // アクセス修飾子の記載がないが継承されてるのでPersonクラスのものがそのまま適応されます。
    class Employee extends Person {
      constructor(name: string, age: number, email: string, public department: string) {
        super(name, age, email);
      }
    
      public getEmployeeInfo() {
        console.log(`名前: ${this.name}, 部署: ${this.department}, Email: ${this.getEmail()}`);
        // console.log(this.getAge()); // エラー: 'getAge'はprivateなため、'Person'外部からアクセスできません
      }
    }
    
    const person = new Person("太郎", 30, "taro@example.com");
    person.greet(); // こんにちは、私の名前は太郎です。
    // console.log(person.age); // エラー: 'age'はprivateなため、'Person'外部からアクセスできません
    // console.log(person.email); // エラー: 'email'はprotectedなため、'Person'外部からアクセスできません
    
    const employee = new Employee("花子", 28, "hanako@example.com", "開発部");
    employee.getEmployeeInfo(); // 名前: 花子, 部署: 開発部, Email: hanako@example.com

Exercise2 "ペット情報取得"

クラスの概要

Pet クラス

  • 概要: ペットの名前、種類、年齢を管理し、情報を提供するクラス。
  • メソッド:
    • constructor(name: string, type: string, age: number): ペットの名前、種類、年齢を設定。
    • getName(): ペットの名前を取得。
    • getType(): ペットの種類を取得。
    • getAge(): ペットの年齢を取得。
    • getInfo(): ペットの名前、種類、年齢の情報を文字列で取得。
    • haveBirthday(): ペットの誕生日を祝って年齢を1歳増やし、メッセージを表示。
class Pet {
    constructor(private name: string, private type: string, private age: number) {}

    public getName(): string {
        return this.name;
    }

    public getType(): string {
        return this.type;
    }

    public getAge(): number {
        return this.age;
    }

    public getInfo(): string {
        return `${this.name} is a ${this.age} year old ${this.type}.`;
    }

    public haveBirthday(): void {
        this.age++;
        console.log(`Happy Birthday, ${this.name}! You are now ${this.age} years old.`);
    }
}

// インスタンス化と使用例
const myPet = new Pet("Fluffy", "cat", 2);
console.log(myPet.getInfo()); // Fluffy is a 2 year old cat.
myPet.haveBirthday(); // Happy Birthday, Fluffy! You are now 3 years old.
console.log(myPet.getInfo()); // Fluffy is a 3 year old cat.

Exercise3 "SetterとGetter"

class Person {
  private _age: number;
  private _name: string;

  constructor(name: string, age: number) {
// ここでアンダーバーを使用しているのでプライベート変数となりカプセル化されるので、インスタンス化の際はsetterは通さずに引数がそのまま渡される
    this._name = name;
    this._age = age;
  }

  get name(): string {
    return this._name;
  }

  set name(value: string) {
    if (value.length < 3) {
      console.log("名前は3文字以上である必要があります。");
    } else {
      this._name = value;
    }
  }

  get age(): number {
    return this._age;
  }

  set age(value: number) {
    if (value < 0 || value > 150) {
      console.log("年齢は0以上150以下である必要があります。");
    } else {
      this._age = value;
    }
  }
}

const person = new Person("太郎", 30);
// インスタンス化の際はsetterは発火せず、getterが発火
console.log(person.name); // 太郎
console.log(person.age); // 30

// 新しく値を設定する場合はsetterが発火して制御がかかる
person.name = ""; // 名前は3文字以上である必要があります。
person.age = -5; // 年齢は0以上150以下である必要があります。

console.log(person.name); // 太郎(変更されない)
console.log(person.age); // 30(変更されない)

Exercise4 "乗り物"

クラスの概要

Vehicule クラス

  • 概要: 車両を表現するクラスで、速度やブランドを管理します。
  • メソッド:
    • constructor(speed: number, _brand: string): 車両の速度とブランド名を設定。
    • set brand(newBrand: string): ブランド名を設定。最初の文字を大文字、残りを小文字に変換。
    • get brand(): 設定されたブランド名を取得。
class Vehicule {
  static color: string = "black";
  constructor(public speed: number, private _brand: string) {}

  set brand(newBrand: string) {
      this._brand = newBrand[0].toUpperCase() + newBrand.slice(1).toLowerCase();
  }

  get brand(): string {
    return this.brand;
  }
}
console.log(Vehicule.color);

class VehiculeAPI {
  static fetchAll() {
    console.log("async request ...");
  }
}

// Vehiculeクラスのインスタンスを作成
const myCar = new Vehicule(120, "Toyota");

// プロパティに直接アクセスし、値を取得
console.log(myCar.speed); // 120

// setterを使用してブランド名を設定
myCar.brand = "nissan";

// getterを使用してブランド名を取得
console.log(myCar.brand); // Nissan

// 静的プロパティにアクセス
console.log(Vehicule.color); // black

// VehiculeAPIクラスの静的メソッドを呼び出し
VehiculeAPI.fetchAll(); // async request ...

中級編

Exercise "銀行口座"

クラス全体の概要

BankAccount クラスは銀行口座の設計図で、以下のようなメソッドが含まれています。

預金 (deposit)
引き出し (withdraw)
残高取得 (getBalance)
他口座への送金 (transferMoney)
月利計算 (getMonthlyInterest)
お気に入り口座の追加 (addAccountToFavorites)
お気に入り口座の取得 (getFavoriteAccounts)
お気に入り口座の削除 (removeFavoriteAccountById)

class BankAccount {
  private favoriteAccounts: BankAccount[] = [];

  constructor(
    private id: number,
    private balance: number,
    private interestRate: number,
    private interestCeiling: number
  ) {}

  deposit(amount: number): void {
    this.balance += amount;
  }

  withdraw(amount: number): void {
    if (this.balance - amount < 0) {
      throw new Error("Insufficient funds.");
    }
    this.balance -= amount;
  }

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

  transferMoney(amount: number, account: BankAccount): void {
    this.withdraw(amount);
    account.deposit(amount);
  }

  getMonthlyInterest(): number {
    if (this.balance > this.interestCeiling) {
      return this.interestCeiling * this.interestRate;
    } else {
      return this.balance * this.interestRate;
    }
  }

  addAccountToFavorites(account: BankAccount): void {
    this.favoriteAccounts.push(account);
  }

  getFavoriteAccounts(): BankAccount[] {
    return this.favoriteAccounts;
  }

  removeFavoriteAccountById(id: number): void {
    const indexToRemove = this.favoriteAccounts.findIndex(
      (account: BankAccount) => account.id === id
    );
    if (indexToRemove === -1) {
      throw new Error("Account not found in favorites");
    }
    this.favoriteAccounts.splice(indexToRemove, 1);
  }
}


const account1 = new BankAccount(1, 40000, 0.01, 50000);
const account2 = new BankAccount(2, 100000, 0.01, 50000);

account1.addAccountToFavorites(account2);

account1.transferMoney(20000, account1.getFavoriteAccounts()[0]);

try {
  account1.withdraw(25000);
} catch (err: unknown) {
  console.log("Error : ", (err as Error).message);
}

console.log("Account 1 mensual interests ", account1.getMensualInterest());
console.log("Balance account 1", account1.getBalance());

account1.removeFavoriteAccountById(2);

Exercise2 "タスク管理クラス"

クラスの概要

Task クラス

  • 概要: タスクの情報を管理するクラス。
  • メソッド:
    • constructor(id: number, title: string, description: string): タスクのID、タイトル、説明を設定。
    • getId(): タスクのIDを取得。
    • getTitle(): タスクのタイトルを取得。
    • getDescription(): タスクの説明を取得。
    • isCompleted(): タスクの完了状態を取得。
    • markAsCompleted(): タスクを完了状態に設定。
    • toString(): タスクの情報を文字列で取得。

TaskManager クラス

  • 概要: 複数のタスクを管理するクラス。
  • メソッド:
    • addTask(title: string, description: string): 新しいタスクを追加。
    • removeTask(id: number): 指定したIDのタスクを削除。
    • markTaskAsCompleted(id: number): 指定したIDのタスクを完了状態に設定。
    • listTasks(): すべてのタスクを一覧表示。
class Task {
    constructor(
        private id: number,
        private title: string,
        private description: string,
        private completed: boolean = false
    ) {}

    public getId(): number {
        return this.id;
    }

    public getTitle(): string {
        return this.title;
    }

    public getDescription(): string {
        return this.description;
    }

    public isCompleted(): boolean {
        return this.completed;
    }

    public markAsCompleted(): void {
        this.completed = true;
    }

    public toString(): string {
        return `[${this.completed ? "X" : " "}] ${this.title}: ${this.description}`;
    }
}

class TaskManager {
    private tasks: Task[] = [];
    private nextId: number = 1;

    public addTask(title: string, description: string): void {
        const task = new Task(this.nextId++, title, description);
        this.tasks.push(task);
    }

    public removeTask(id: number): void {
        this.tasks = this.tasks.filter(task => task.getId() !== id);
    }

    public markTaskAsCompleted(id: number): void {
        const task = this.tasks.find(task => task.getId() === id);
        if (task) {
            task.markAsCompleted();
        }
    }

    public listTasks(): void {
        this.tasks.forEach(task => {
            console.log(task.toString());
        });
    }
}

// インスタンス化と使用例
const taskManager = new TaskManager();
taskManager.addTask("食材を買う", "牛乳, お米, 鶏肉");
taskManager.addTask("英語学習", "午前と午後に1レッスンずつ");
taskManager.listTasks();

taskManager.markTaskAsCompleted(1); // [X] 食材を買う: 牛乳, お米, 鶏肉
taskManager.listTasks();

taskManager.removeTask(2);
taskManager.listTasks();

Exercise3 "連絡先管理クラス"

クラスの概要

Contact クラス

  • 概要: 連絡先情報を管理するクラス。
  • メソッド:
    • constructor(id: number, name: string, email: string, phone: string): ID、名前、メール、電話番号を設定。
    • getId(): IDを取得。
    • getName(): 名前を取得。
    • getEmail(): メールアドレスを取得。
    • getPhone(): 電話番号を取得。
    • toString(): 連絡先情報を文字列で取得。

ContactManager クラス

  • 概要: 複数の連絡先を管理するクラス。
  • プロパティ:
    • contacts(プライベート): 管理する連絡先のリスト。
    • nextId(プライベート): 次に追加する連絡先のID(初期値は1)。
  • メソッド:
    • addContact(name: string, email: string, phone: string): 新しい連絡先を追加。
    • removeContact(id: number): 指定したIDの連絡先を削除。
    • searchContact(name: string): 名前で連絡先を検索。
    • listContacts(): すべての連絡先を一覧表示。
class Contact {
    constructor(
        private id: number,
        private name: string,
        private email: string,
        private phone: string
    ) {}

    public getId(): number {
        return this.id;
    }

    public getName(): string {
        return this.name;
    }

    public getEmail(): string {
        return this.email;
    }

    public getPhone(): string {
        return this.phone;
    }

    public toString(): string {
        return `ID: ${this.id}, Name: ${this.name}, Email: ${this.email}, Phone: ${this.phone}`;
    }
}

class ContactManager {
    private contacts: Contact[] = [];
    private nextId: number = 1;

    public addContact(name: string, email: string, phone: string): void {
        const contact = new Contact(this.nextId++, name, email, phone);
        this.contacts.push(contact);
    }

    public removeContact(id: number): void {
        this.contacts = this.contacts.filter(contact => contact.getId() !== id);
    }

    public searchContact(name: string): Contact[] {
        return this.contacts.filter(contact => contact.getName().toLowerCase().includes(name.toLowerCase()));
    }

    public listContacts(): void {
        this.contacts.forEach(contact => {
            console.log(contact.toString());
        });
    }
}

// インスタンス化と使用例
const contactManager = new ContactManager();
contactManager.addContact("Alice Johnson", "alice@example.com", "123-456-7890");
contactManager.addContact("Bob Smith", "bob@example.com", "987-654-3210");
contactManager.listContacts();

console.log("Search results for 'alice':");
contactManager.searchContact("alice").forEach(contact => console.log(contact.toString()));

contactManager.removeContact(1);
contactManager.listContacts();

上級編

Exercise "いろいろな本"

ここではTypeScriptのinterfaceを使ったクラス設計の練習として、BookEBookAudioBookの3つのクラスを作成し、共通のBookInterfaceを使用する例を紹介します。この例では、異なる種類の本(本、電子書籍、オーディオブック)を管理するためのクラスを作成し、それぞれの特有のメソッドとプロパティを実装します。

クラスの概要

BookInterface

  • 概要: 本の基本的なプロパティとメソッドを定義するインターフェース。
  • メソッド:
    • read(): 本を読んだことを記録するメソッド。
    • getDetails(): 本の詳細情報を文字列で取得するメソッド。

Book クラス

  • 概要: 物理的な本を表現するクラスで、BookInterface を実装。
  • メソッド:
    • constructor(title: string, author: string, pages: number): タイトル、著者、ページ数を設定。
    • read(): 本を読んだことを記録。
    • getDetails(): 本の詳細情報を文字列で取得。

EBook クラス

  • 概要: 電子書籍を表現するクラスで、BookInterface を実装。
  • メソッド:
    • constructor(title: string, author: string, pages: number, format: string): タイトル、著者、ページ数、フォーマットを設定。
    • read(): 電子書籍を読んだことを記録。
    • getDetails(): 本の詳細情報を文字列で取得。
    • getFormat(): 電子書籍のフォーマットを取得。

AudioBook クラス

  • 概要: オーディオブックを表現するクラスで、BookInterface を実装。
  • メソッド:
    • constructor(title: string, author: string, pages: number, duration: number): タイトル、著者、ページ数、再生時間を設定。
    • read(): オーディオブックを聞いたことを記録。
    • getDetails(): 本の詳細情報を文字列で取得。
    • getDuration(): オーディオブックの再生時間を取得。
// まず、すべての本に共通するプロパティとメソッドを定義する`BookInterface`を作成します。
interface BookInterface {
    title: string;
    author: string;
    pages: number;
    read(): void;
    getDetails(): string;
}

// Bookクラス
// implements キーワードは、TypeScriptでクラスが特定のインターフェースを実装することを示すために使用されます
class Book implements BookInterface {
    constructor(
        public title: string,
        public author: string,
        public pages: number,
        private isRead: boolean = false
    ) {}

    read(): void {
        this.isRead = true;
        console.log(`You have read "${this.title}" by ${this.author}.`);
    }

    getDetails(): string {
        return `${this.title} by ${this.author}, ${this.pages} pages. ${this.isRead ? "Already read" : "Not read yet"}.`;
    }
}

// EBookクラス

class EBook implements BookInterface {
    constructor(
        public title: string,
        public author: string,
        public pages: number,
        private format: string,
        private isRead: boolean = false
    ) {}

    read(): void {
        this.isRead = true;
        console.log(`You have read the eBook "${this.title}" by ${this.author}.`);
    }

    getDetails(): string {
        return `${this.title} by ${this.author}, ${this.pages} pages, format: ${this.format}. ${this.isRead ? "Already read" : "Not read yet"}.`;
    }

    getFormat(): string {
        return this.format;
    }
}

// AudioBookクラス
class AudioBook implements BookInterface {
    constructor(
        public title: string,
        public author: string,
        public pages: number,
        private duration: number, // Duration in minutes
        private isRead: boolean = false
    ) {}

    read(): void {
        this.isRead = true;
        console.log(`You have listened to the audiobook "${this.title}" by ${this.author}.`);
    }

    getDetails(): string {
        return `${this.title} by ${this.author}, ${this.pages} pages, duration: ${this.duration} minutes. ${this.isRead ? "Already listened" : "Not listened yet"}.`;
    }

    getDuration(): number {
        return this.duration;
    }
}

// 使用例
const physicalBook = new Book("The Great Gatsby", "F. Scott Fitzgerald", 180);
const eBook = new EBook("1984", "George Orwell", 328, "PDF");
const audioBook = new AudioBook("Becoming", "Michelle Obama", 400, 1140);

console.log(physicalBook.getDetails());
physicalBook.read();
console.log(physicalBook.getDetails());

console.log(eBook.getDetails());
eBook.read();
console.log(eBook.getDetails());

console.log(audioBook.getDetails());
audioBook.read();
console.log(audioBook.getDetails());

まとめ

この例では、TypeScriptのinterfaceを使用して、共通のプロパティとメソッドを持つ複数のクラスを設計しました。これにより、異なる種類の本を統一的に管理し、それぞれの特有のプロパティやメソッドを実装することができます。

Exercise2 "魔法使い"

ここではTypeScriptのenum型を使用した魔法使いClassの設計を行います。

列挙型(Enum)は、関連する定数の集合に名前を付けて、これらの定数をグループ化するための構造です。TypeScriptでは列挙型を使用して、コードの可読性と保守性を向上させることができます。列挙型は、数値列挙型と文字列列挙型の2種類があります。

数値列挙型

数値列挙型は、列挙型の各メンバーに数値(デフォルトでは0から始まる連続した数値)を割り当てます。

enum Direction {
    Up,    // 0
    Down,  // 1
    Left,  // 2
    Right  // 3
}

let dir: Direction = Direction.Up;
console.log(dir); // 0

数値列挙型のカスタム値

デフォルトの数値以外の値を割り当てることもできます。

enum Direction {
    Up = 1,
    Down,      // 2
    Left = 4,
    Right      // 5
}

let dir: Direction = Direction.Left;
console.log(dir); // 4

文字列列挙型

文字列列挙型は列挙型の各メンバーに文字列を割り当てます。

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT"
}

let dir: Direction = Direction.Up;
console.log(dir); // "UP"

クラスの概要

Spell クラス

  • 概要: 呪文の基本プロパティとメソッドを定義する抽象クラス。直接インスタンス化できない。

FireSpellName TypeScript enum型(列挙型)

  • 概要: 火の呪文の名前を定義。

FrostSpellName TypeScript enum型(列挙型)

  • 概要: 氷の呪文の名前を定義。

FireSpell クラス

  • 概要: 火の呪文を表現するクラスで、Spell クラスを継承。
  • メソッド:
    • constructor(name: FireSpellName): 火の呪文の名前を設定。
    • cast(): 呪文をキャストし、ダメージメッセージを表示。

FrostSpell クラス

  • 概要: 氷の呪文を表現するクラスで、Spell クラスを継承。
  • メソッド:
    • constructor(name: FrostSpellName): 氷の呪文の名前を設定。
    • cast(): 呪文をキャストし、遅延メッセージを表示。

SpellName<S> 型エイリアス

  • 概要: SFireSpell なら FireSpellName 型を、そうでなければ FrostSpellName 型を返す条件付き型。

Wizard<S extends Spell> クラス

  • 概要: 魔法使いを表現するクラスで、呪文を扱う。
    • spellBook(プライベート): 持っている呪文のリスト。
  • メソッド:
    • constructor(spellBook: S[]): 呪文のリストを設定。
    • castAllAtOnce(): 持っているすべての呪文をキャスト。
    • castFromSpellBook(name: SpellName<S>): 指定された名前の呪文をキャスト。
// abstractだからインスタンス化できないよ
abstract class Spell {
  constructor(private _name: string) {}

  get name() {
    return this._name;
  }

 // サブクラスでメソッドの実装を強制
  abstract cast(): void;
}

// 火の呪文の型
enum FireSpellName {
  FireBolt = "Fire Bolt",
  FireWall = "Fire Wall",
  BigBang = "Big Bang",
}

// 氷の呪文の型
enum FrostSpellName {
  FrostBolt = "Frost Bolt",
  Blizzard = "Blizzard",
}

// 火の呪文クラス
class FireSpell extends Spell {
  readonly burningDamage = 20;
  constructor(name: FireSpellName) {
    super(name);
  }
  cast() {
    console.log(
      this.name,
      `fire! 敵を燃やしています! 敵は ${this.burningDamage} のダメージを受けました`
    );
  }
}

// 氷の呪文クラス
class FrostSpell extends Spell {
  readonly slowingRate = 0.5;

  constructor(name: FrostSpellName) {
    super(name);
  }

  cast() {
    console.log(
      this.name,
      `fire! 敵を凍らせています! 敵は ${this.burningDamage} のダメージを受けました`
    );
  }
}

// 条件付き型でSがFireSpell 型に代入可能かどうかをチェック
type SpellName<S> = S extends FireSpell
  ? FireSpellName
  : FrostSpellName;

class Wizard<S extends Spell> {
  private spellBook: S[] = [];

  constructor(spellBook: S[]) {
    this.spellBook = spellBook;
  }

// 持っているすべての呪文を実行するメソッド
  castAllAtOnce() {
    this.spellBook.forEach((spell: S) => {
      spell.cast();
    });
  }

  castFromSpellBook(name: SpellName<S>) {
    const spell = this.spellBook.find(
      (spell) => spell.name == name
    );
    if (spell) {
      spell.cast();
    } else {
      throw new Error(
        "あなたはこの魔法は使えません !"
      );
    }
  }
}

// 火属性の呪文を持った魔法使いだけインスタンス化
const fireSpells: FireSpell[] = [
  new FireSpell(FireSpellName.FireBolt),
  new FireSpell(FireSpellName.FireWall),
  new FireSpell(FireSpellName.BigBang),
];

const fireWizard = new Wizard(fireSpells);
fireWizard.castFromSpellBook(FireSpellName.FireBolt);

// 氷属性の呪文を持った魔法使いだけインスタンス化
const frostSpells: FrostSpell[] = [
  new FrostSpell(FrostSpellName.FrostBolt),
  new FrostSpell(FrostSpellName.Blizzard),
];

const frostWizard = new Wizard(frostSpells);

frostWizard.castFromSpellBook(FrostSpellName.Blizzard);

Exercise3 "履歴管理システム"

ここではTypeScriptのジェネリクス型を使ったクラスの設計として、「履歴管理システム」を作成します。このシステムは、任意の型の値の履歴を追跡し、元に戻したり、やり直したりする機能があります。
クラスの設計自体はシンプルなのですが、ジェネリクス型が入ってくると初学者の方はとっつきにくくなるので、ゆっくり調べながら理解していきましょう。

ジェネリクス型の説明

ジェネリクス型(Generics)とは、型をパラメータ化することで、クラスや関数がさまざまな型に対して動作するようにする仕組みです。ジェネリクスを使用することで、コードの再利用性と型安全性を高めることができます。

Generics型(ジェネリクス)の基本例

function identity<T>(arg: T): T {
    return arg;
}

let output1 = identity<string>("myString"); // output1の型はstring
let output2 = identity<number>(100); // output2の型はnumber

上記のように、ジェネリクスを使うことで、identity 関数は引数の型に依存して、その型に合わせた出力を返すことができます。
もっと簡単に言うとクラスや関数の型を引数に応じて動的に決定できるって感じです。

クラスの概要

Undoable<T> インターフェース

  • 概要: ジェネリクス型 T を使用した履歴管理の基本的な操作を定義するインターフェース。
  • メソッド:
    • add(value: T): 値を履歴に追加します。
    • undo(): T | undefined: 履歴から直近の操作を取り消します。
    • redo(): T | undefined: 取り消した操作を再実行します。
    • getHistory(): T[]: 履歴の全体を取得します。

HistoryManager<T> クラス

  • 概要: Undoable<T> インターフェースを実装し、ジェネリクス型 T を使用して任意の型の値の履歴管理を行うクラス。
  • プロパティ:
    • history: T[]: すべての履歴を保存する配列。
    • undoStack: T[]: 取り消し操作を保存するスタック。
    • redoStack: T[]: 再実行操作を保存するスタック。
  • メソッド:
    • add(value: T): 値を履歴に追加し、取り消しスタックに保存。
    • undo(): T | undefined: 直近の操作を取り消し、再実行スタックに保存。
    • redo(): T | undefined: 取り消した操作を再実行し、取り消しスタックに戻す。
    • getHistory(): T[]: 履歴全体を取得。
// インターフェースの定義
// まず、ジェネリクスを使用したインターフェースを定義します。
interface Undoable<T> {
    add(value: T): void;
    undo(): T | undefined;
    redo(): T | undefined;
    getHistory(): T[];
}

// 履歴管理クラス
// 次に、`Undoable` インターフェースを実装する `HistoryManager` クラスを作成します。
class HistoryManager<T> implements Undoable<T> {
    private history: T[] = [];
    private undoStack: T[] = [];
    private redoStack: T[] = [];

    add(value: T): void {
        this.history.push(value);
        this.undoStack.push(value);
        this.redoStack = []; // 新しい値が追加されたら、redoスタックをクリアする
    }

    undo(): T | undefined {
        const value = this.undoStack.pop();
        if (value !== undefined) {
            this.redoStack.push(value);
        }
        return value;
    }

    redo(): T | undefined {
        const value = this.redoStack.pop();
        if (value !== undefined) {
            this.undoStack.push(value);
        }
        return value;
    }

    getHistory(): T[] {
        return this.history;
    }
}
// 使用例
// 文字列の履歴管理
const stringHistory = new HistoryManager<string>();
stringHistory.add("First");
stringHistory.add("Second");
stringHistory.add("Third");

console.log("History:", stringHistory.getHistory()); // ["First", "Second", "Third"]
console.log("Undo:", stringHistory.undo()); // "Third"
console.log("Redo:", stringHistory.redo()); // "Third"

// 数値の履歴管理
const numberHistory = new HistoryManager<number>();
numberHistory.add(1);
numberHistory.add(2);
numberHistory.add(3);

console.log("History:", numberHistory.getHistory()); // [1, 2, 3]
console.log("Undo:", numberHistory.undo()); // 3
console.log("Redo:", numberHistory.redo()); // 3


// オブジェクトの履歴管理

interface State {
    x: number;
    y: number;
}

const stateHistory = new HistoryManager<State>();
stateHistory.add({ x: 0, y: 0 });
stateHistory.add({ x: 1, y: 1 });
stateHistory.add({ x: 2, y: 2 });

console.log("History:", stateHistory.getHistory()); // [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}]
console.log("Undo:", stateHistory.undo()); // {x: 2, y: 2}
console.log("Redo:", stateHistory.redo()); // {x: 2, y: 2}

終わりに

TypeScript初学者にとっては最後の方は少し難しかったかもしれませんが、1度で理解する必要はないので、わからないところは調べながら少しずつ理解できればいいと思います。

慣れてきたらハンズオンで学習してより理解を深めていきましょう!

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?