👀

ジェネリッククラス【個人学習まとめ】

に公開

ジェネリッククラス

ジェネリッククラスの宣言、使用方法を理解できていなかったのでまとめてみました。

宣言

class DataStorage<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  getItem(index: number): T {
    return this.items[index];
  }

  getAllItems(): T[] {
    return [...this.items];
  }
}

上記のDataStorageクラスは、型変数にTを持っています。この型変数Tによりどんな型のデータも受け取ることができます。

DataStorageクラスはプライベート変数のitemsを持ちます。これはTの配列です。
また、3 つのメソッドを持っています。

  • addメソッド
    • itemsに新しいデータを追加する
  • getItemメソッド
    • items内の指定したデータを取得する
  • getAllItemsメソッド
    • items内のすべてのデータを取得する
  • deleteItemメソッド
    • 指定したデータを削除した配列を取得する

DataStorateクラスにより、特定のデータ型に限定されずに再利用可能なデータストレージの実装が可能になりました。

インスタンス化

作成したジェネリッククラスのインスタンス化(実体化)してみましょう!

let numberStorage = new DataStorage<number>();
numberStorage.add(10);

DataStorageクラスの型引数にnumber型を指定し、インスタンス化しました。これにより、このインスタンスnumberStoragenumber型専用になります。
仮に文字列のデータをaddメソッドを使って追加しようとしても、型の不一致によりエラーになります。

numberStorage.add("文字列は追加できない");
→ 型 'string' の引数を型 'number' のパラメーターに割り当てることはできません。

addメソッド以外の機能も動作するか簡単に確認してみましょう。

numberStorage.add(11);
numberStorage.add(12);
numberStorage.add(13);
numberStorage.add(14);
numberStorage.add(15);
console.log(numberStorage.getItem(0));10
console.log(numberStorage.getAllItems());[ 10, 11, 12, 13, 14, 15 ]
console.log(numberStorage.deleteItem(11));[ 10, 12, 13, 14, 15 ]

問題ないですね。
number型のストレージを作りましたが、他の型でも問題なく動作するでしょうか?試してみましょう。

let greetStorage = new DataStorage<string>();
greetStorage.add("おはよう");
greetStorage.add("こんにちは");
greetStorage.add("こんばんは");

console.log(greetStorage.getAllItems());
["おはよう", "こんにちは", "こんばんは"];

上記の例では、string型を指定してDataStorageをインスタンス化し、greetStorageを作成しました。
string型を指定したので、string専用のストレージです。

型推論(+コンストラクタの追加)

新たなジェネリッククラスDataStorageWithConstructorを作成しました。最初に紹介したジェネリッククラスにコンストラクタを追加したものになります。

class DataStorageWithConstructor<T = number> {
  private items: T[] = [];

  //コンストラクタを追加
  constructor(initialItems?: T[]) {
    if (initialItems) {
      this.items.push(...initialItems);
    }
  }

  add(item: T): void {
    this.items.push(item);
  }

  getItem(index: number): T {
    return this.items[index];
  }

  getAllItems(): T[] {
    return [...this.items];
  }

  deleteItem(item: T): T[] {
    const result = this.items.filter((data) => data !== item);
    return result;
  }
}

このコンストラクタを追加したことにより、インスタンス化直後に初期値を設定することができ、TypeScirpt による型推論も初期値から行うことができるようになりました。

let nameStorage = new DataStorageWithConstructor(["佐藤", "千葉"]);
console.log(nameStorage.getAllItems());[ '佐藤', '千葉' ]

上記の例だと、インスタンス化時に型引数を省略しています。初期値に基づき、型引数Tstring型であると TypeScript が型推論してくれるわけですね。

継承

ジェネリッククラスを継承してサブクラスを作成することができます。
最初に作成したDataStorageクラスを継承してDataStorageStrLoggerというサブクラスを作成します。

class DataStorageStrLogger extends DataStorage<string> {
  printAllItems(): void {
    const allItems = this.getAllItems();
    console.log(`保存しているデータ: ${allItems}`);
  }
}

DataStorageStrLoggerにはprintAllItemsメソッドを追加します。動作としては保存されているデータをコンソールで出力します。
itemというフィールドはDataStorageクラスのプライベートフィールドです。DataStorageStrLoggerクラスから直接アクセスすることはできないので、DataStorageクラスのgetAllItemsメソッドを経由してitemsのデータを取得します。

ジェネリッククラスを継承する場合、型引数を指定しなければなりません。上記の例では、string型を指定していますね。
仮に型引数を指定しなかった場合はエラーとなります。

class ErrorStorage extends DataStorage{
→ ジェネリック型 'DataStorage<T>' には 1 個の型引数が必要です。
(省略)
}

サブクラスからスーパークラスに型引数を渡すことができる

DataStorageクラスを継承してDataStorageSampleクラスを作成します。型引数をはDataStorageと同じようにTとします。
さらにgetFirstItemメソッドを追加します。itemsの最初の値を取得します。

class DataStorageSample<T> extends DataStorage<T> {
  printAllItems(): void {
    const allItems = this.getAllItems();
    console.log(`保存しているデータ: ${allItems}`);
  }

  getFirstItem(): T {
    return this.getItem(0);
  }
}

DataStorageSampleクラスをインスタンス化するするときに型を指定すると、その型情報はスーパークラスであるDataStorageクラスも同じ型が指定されます。

let sampleStorage = new DataStorageSample<string>();

上記の例だと、サブクラスで指定したstring型がスーパークラスのDataStorageに渡される。ということになります。
このことにより、

  • addメソッドの引数はstring型であること
  • getFirstItemの戻り値はstring型であること

が期待されます。
もし、sampleStorageboolean型のデータを追加しようとしてもエラーになります。

sampleStorage.add(true);
→ 型 'boolean' の引数を型 'string' のパラメーターに割り当てることはできません。

DataStorageSampleクラスがstring型でインスタンス化されていることによって、スーパークラスのメソッドaddstring型のみを受け付ける。ということになります。

クラスによるジェネリックインターフェイスの実装方法

ここまでの内容を応用して、インターフェイス(設計書)を作成し実装してみましょう。
インターフェイスであるIStorageを実装します。型変数はTとして、どんな型でも扱えるようにしておきます。それぞれのメンバーの型情報にも型変数Tを利用します。

interface IStorage<T> {
  add(item: T): void;
  getItem(index: number): T;
  getAllItems(): T[];
  deleteItem(item: T): T[];
}

ではこのインターフェイスの機能を持つクラスを実装してみましょう。
クラス名はStorageClassです。

class StorageClass<T> implements IStorage<T> {
  private items: T[] = [];

  add(item: T) {
    this.items.push(item);
  }

  getItem(index: number): T {
    return this.items[index];
  }

  getAllItems(): T[] {
    return [...this.items];
  }
}

implements IStorage<T>でインターフェイスIStorageを実装したことにより、StorageClassではインターフェイスのメンバーを必ず保持していなければなりません。しかし、deleteItemメソッドが実装されていないため、このままではエラーとなります。
(エラー内容: クラス 'StorageClass<T>' はインターフェイス 'IStorage<T>' を正しく実装していません。
プロパティ 'deleteItem' は型 'StorageClass<T>' にありませんが、型 'IStorage<T>' では必須です。)

class StorageClass<T> implements IStorage<T> {
  private items: T[] = [];

  add(item: T) {
    this.items.push(item);
  }

  getItem(index: number): T {
    return this.items[index];
  }

  getAllItems(): T[] {
    return [...this.items];
  }

+  deleteItem(item: T): T[] {
+    const result = this.items.filter((data) => data !== item);
+    return result;
  }
}

これで正しく実装することができました。

Discussion