TypeScriptでジェネリッククラスを作成する方法と活用例をやさしく解説!
生徒
「先生!TypeScriptでジェネリクスってよく聞くんですが、クラスでも使えるんですか?」
先生
「もちろん使えます。ジェネリクスは、関数だけでなくクラスにも使うことができますよ。」
生徒
「クラスにジェネリクスを使うと、どんなメリットがあるんですか?」
先生
「例えば、データの型を自由に変えられる柔軟なクラスを作れるんです。実際にコードを見ながら学んでいきましょう!」
1. ジェネリッククラスとは?
TypeScript(タイプスクリプト)のジェネリッククラス(Generic Class)とは、型をあとから自由に指定できるクラスのことです。ジェネリクス(Generics)というのは、英語で「一般的な」という意味を持ち、どんな型でも扱える柔軟な仕組みを指します。
通常のクラスは、特定の型に固定されてしまいます。たとえば「数値だけを扱うクラス」や「文字列だけを扱うクラス」などです。しかし、ジェネリクスを使えば、ひとつのクラスをどんな型にも対応させることができます。
たとえるなら、「おもちゃ箱」がどんなおもちゃでも入れられるように、型を限定しない「箱」を作るようなイメージです。
2. ジェネリッククラスの基本構文
TypeScriptでジェネリッククラスを作るときは、クラス名の後ろに山かっこのような記号「<>」を使い、その中に型の変数を指定します。この型変数は、実際にクラスを使うときに具体的な型を指定できます。
class Box<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
この例では、Tという文字が「型の入れ物」として使われています。Tは「Type(型)」の略で、どんな名前でも構いません(例えば U や K でもOKです)。
3. ジェネリッククラスの使い方
作ったジェネリッククラスを使うときには、具体的な型を指定してインスタンス(クラスを使って作ったオブジェクト)を生成します。
let stringBox = new Box<string>("こんにちは");
let numberBox = new Box<number>(123);
console.log(stringBox.getValue()); // "こんにちは"
console.log(numberBox.getValue()); // 123
こんにちは
123
このように、<string>や<number>を指定することで、それぞれ文字列用・数値用のクラスとして利用できます。
つまり、ジェネリッククラスを使えば、同じクラスを何度も作る必要がなくなり、コードの再利用性が高まります。
4. 型推論を使ったジェネリクスの便利な使い方
TypeScriptの強力な機能のひとつに型推論(Type Inference)があります。これは、TypeScriptが自動的に値の型を判断してくれる機能です。
ジェネリッククラスでも、型推論を使えば、わざわざ型を明示しなくても動作します。
let autoBox = new Box("TypeScript学習中!");
console.log(autoBox.getValue());
TypeScript学習中!
このように、TypeScriptが"TypeScript学習中!"という文字列を見て、自動的にstring型として扱ってくれます。
5. 複数の型パラメータを使うジェネリッククラス
ジェネリクスでは、ひとつのクラスに複数の型パラメータを設定することも可能です。たとえば「キー」と「値」をセットで管理するようなクラスを作るときに便利です。
class Pair<K, V> {
constructor(private key: K, private value: V) {}
getKey(): K {
return this.key;
}
getValue(): V {
return this.value;
}
}
let pair = new Pair<string, number>("年齢", 30);
console.log(pair.getKey()); // "年齢"
console.log(pair.getValue()); // 30
年齢
30
このように、KとVという2つの型パラメータを使うことで、「キーが文字列」「値が数値」といった形のクラスを作ることができます。
複数の型を扱うことで、より現実的なデータ構造(例えば「IDとデータの組み合わせ」など)を安全に表現できます。
6. ジェネリッククラスを使うメリット
ここで、ジェネリッククラスを使う主なメリットを整理しておきましょう。
- 型の再利用性が高い: 同じクラスをどんな型でも使えるので、無駄なコードを減らせます。
- 型の安全性が向上: 誤った型を入れるとエラーになるため、実行時のバグを防げます。
- 保守性が高まる: どんな型を扱うかが明確になり、他の人が読んでも分かりやすいコードになります。
TypeScriptのジェネリクスは、「柔軟で安全」なプログラミングを実現するための重要な機能です。慣れてくると、実務でも非常によく使うテクニックになります。
まとめ
TypeScriptのジェネリッククラスで学んだことのおさらい
TypeScriptでジェネリッククラスを定義するときの基本的な考え方はとてもシンプルでしたが、実際にコードを書きながら確認していくことで、クラスと型の関係をより深く理解できるようになりました。この記事で学んだとおり、ジェネリクスを使ったクラスは、特定の型だけに縛られずに同じロジックをさまざまな値に対して適用できる柔軟な仕組みでした。文字列を扱う場合も数値を扱う場合も、ひとつのジェネリッククラスで共通のメソッドを呼び出せるという体験は、型付き言語であるTypeScriptならではの安心感につながります。
とくに、Boxクラスのようにひとつの値を安全に保持して取り出すだけのシンプルな構造でも、ジェネリック型パラメータを導入することで、テストデータや設定情報やメッセージなどいろいろな用途に応用できることに気づけたはずです。これまでは用途ごとに似たようなクラスやインターフェースを複数定義していた人も、ジェネリッククラスを使えば共通化できる場面が多いと感じたのではないでしょうか。ジェネリッククラスを意識して設計すると、自然と重複の少ない読みやすいコードになり、保守もしやすくなります。
さらに、コンストラクタやメソッドの引数と戻り値に同じ型パラメータを使うことで、「入れて出す値は必ず同じ型になる」という約束を型レベルで表現できる点も大きなポイントでした。これにより、誤って別の型の値を代入してしまうミスを事前に防げるようになります。型推論が効くため、毎回すべての型を明示しなくてもTypeScriptコンパイラが自動的に判断してくれることも、日々の開発を楽にしてくれる重要な仕組みです。
また、複数の型パラメータを受け取るPairクラスの例からは、「キーと値」「名前と年齢」「識別子と本体」のような二つの情報をセットで扱う場面でジェネリッククラスがとても役立つことが分かりました。片方だけ型が違うケースや、用途によってキーと値の型の組み合わせが変わるケースでも、クラス本体の定義はそのままで、利用時に型を差し替えるだけで対応できるため、設計の自由度がぐっと高まります。
さらに一歩踏み込んだ使い方として、ジェネリッククラスとインターフェースやジェネリック関数を組み合わせることで、アプリ全体のデータモデルやビジネスロジックを統一的に表現できるようになります。データを格納するリポジトリクラスや、イベントを扱うマネージャークラスなども、ジェネリクスを使っておけば、後から扱う型が増えたときにもスムーズに拡張していけるでしょう。小さなサンプルから始めて、徐々に自分のプロジェクトのクラス設計に取り入れていくことが上達への近道です。
また、ジェネリッククラスは単に便利というだけでなく、チーム開発におけるコミュニケーションの助けにもなります。型パラメータの名前やクラス名を工夫することで、「このクラスはどんな型のどんなデータを扱うのか」が自然と読み取れるようになり、コードレビューや設計の相談もしやすくなります。コメントやドキュメントに頼らなくても、型情報そのものが仕様を説明してくれるような状態を目指すと、TypeScriptの強みを最大限に活かせます。
もちろん、最初から完璧なジェネリッククラスを設計する必要はありません。まずは通常のクラスを書いてみて、「ここは型だけが違う同じ処理を何度も書いているな」と感じたタイミングで、ジェネリクスに置き換えられないか考えてみるとよいでしょう。既存のコードを少しずつリファクタリングしながら、型パラメータの使い方や制約の付け方を試していくことで、自分なりの書きやすいスタイルが身についていきます。
TypeScriptでジェネリッククラスを自在に扱えるようになると、配列やマップやスタックやキューといった基本的なデータ構造も、より自信を持って設計できるようになります。標準ライブラリや他の人が書いたクラスの定義を読むときにも、「このTやKやVはどんな型として使われているのか」を意識して読み解けるようになり、理解のスピードが一段と上がるはずです。日常的に触れる機会の多いクラスから少しずつジェネリクスに慣れていきましょう。
ジェネリッククラスのおさらいサンプルコード
最後に、記事全体で学んだ内容を踏まえた簡単なサンプルコードをもう一度まとめておきます。ひとつのジェネリッククラスで文字列と数値を安全に扱う流れを、実際のコードとして確認してみてください。
class GenericBox<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
setValue(value: T): void {
this.value = value;
}
}
const messageBox = new GenericBox<string>("最初のメッセージ");
console.log(messageBox.getValue());
const pointBox = new GenericBox<number>(100);
pointBox.setValue(200);
console.log(pointBox.getValue());
この例のように、GenericBoxクラスは引数の型と保持する値の型と戻り値の型をすべて同じTで揃えているため、一度型を決めてしまえばクラスの外から誤った型の値を渡すことはできません。同じクラス定義を使い回しながらも、実際の利用場面では文字列用や数値用として自由にインスタンスを作成できるので、コードの見通しも良くなります。慣れてきたら、ここにメソッドを追加して履歴を持たせたり、配列を使って複数の値を管理したりと、自分なりの拡張にも挑戦してみてください。
さらに、複数の型パラメータを取るPairクラスや、制約付きのジェネリクスを組み合わせれば、実務でよく登場する「識別子と値の組み合わせ」「設定名と設定値」「ステータスと結果」といった構造も安全に表現できます。クラスの役割と扱う型の関係を頭の中で整理しながら、少しずつパターンを増やしていくことで、TypeScriptでの設計力が着実に高まっていきます。
生徒
「きょうの学習で、ジェネリッククラスはひとつのクラス定義でいろいろな型を扱える便利な仕組みだということがよく分かりました。同じロジックを何度も書かなくてよくなるので、TypeScriptでアプリケーションを作るときにかなり役立ちそうです。」
先生
「そのとおりですね。とくにBoxクラスやPairクラスのような例から、コンストラクタやメソッドの引数と戻り値を型パラメータで統一しておくと、あとから値の型を変えたくなってもクラス本体をほとんど書き換えずに済むという感覚がつかめたと思います。」
生徒
「はい。最初は山かっこの記号やTという文字に少し戸惑いましたが、使う場面をイメージしながらコードを書いてみると、型の入れ物だと考えれば怖くないと感じました。型推論のおかげで、全部の型を意識しなくても自然に補ってくれるのも心強いです。」
先生
「とても良い気づきですね。今後は自分でクラスを設計するときに、最初からジェネリッククラスとして作るか、あとから共通化できないかを考えてみるとよいでしょう。小さなユーティリティクラスでもジェネリクスを導入してみると、TypeScriptの型システムへの理解がさらに深まりますよ。」
生徒
「これからは配列やマップを扱うときにも、どんな型を包んでいるのか意識してコードを読んでみます。そして、自分のプロジェクトでもジェネリッククラスを少しずつ増やして、読みやすくて安全なコードを書けるように練習していきます。」
先生
「すばらしい意気込みですね。毎日のコーディングのなかで少しずつジェネリクスに触れていけば、自然と使いこなせるようになります。分からないところが出てきたら、また具体的なコードを一緒に確認しながらTypeScriptのジェネリッククラスを深めていきましょう。」