ジェネリッククラス【個人学習まとめ】
ジェネリッククラス
ジェネリッククラスの宣言、使用方法を理解できていなかったのでまとめてみました。
宣言
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
型を指定し、インスタンス化しました。これにより、このインスタンスnumberStorage
はnumber
型専用になります。
仮に文字列のデータを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());
→ [ '佐藤', '千葉' ]
上記の例だと、インスタンス化時に型引数を省略しています。初期値に基づき、型引数T
がstring
型であると 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
型であること
が期待されます。
もし、sampleStorage
にboolean
型のデータを追加しようとしてもエラーになります。
sampleStorage.add(true);
→ 型 'boolean' の引数を型 'string' のパラメーターに割り当てることはできません。
DataStorageSample
クラスがstring
型でインスタンス化されていることによって、スーパークラスのメソッドadd
もstring
型のみを受け付ける。ということになります。
クラスによるジェネリックインターフェイスの実装方法
ここまでの内容を応用して、インターフェイス(設計書)を作成し実装してみましょう。
インターフェイスである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