プログラムの再利用性を高めるためには、型に依存しない柔軟なコードを書くことが重要です。TypeScriptのジェネリクス(Generics)は、型の柔軟性を持ちながらも型安全性を保つための強力な機能です。ジェネリクスを使うことで、異なる型に対応した再利用可能な関数やクラスを作成することができ、かつ型チェックを行うことで安全性を確保できます。
この章では、TypeScriptのジェネリクスの基本的な概念から、関数やクラスへの応用、そして型制約などの高度な使い方までを丁寧に解説します。
7.1 ジェネリクスとは?
ジェネリクスとは、「特定の型に依存しないコードを書くための仕組み」です。通常の関数やクラスでは、引数や戻り値の型をあらかじめ決めておく必要がありますが、ジェネリクスを使うと、後から渡された型に応じて処理を行う柔軟なコードを作成することができます。
例えば、同じ処理を行う関数でも、異なる型の引数を受け取る場合、それぞれの型に対して別々の関数を作成するのは非効率です。ジェネリクスを使うと、同じ関数を複数の型に対して再利用できるようになります。
7.2 ジェネリクスの基本
ジェネリクスを使った関数やクラスでは、型を引数のように扱います。型引数を使って、どの型でも受け取れるように柔軟なコードを作成し、その型がどのように扱われるべきかを定義します。
7.2.1 ジェネリック関数の定義
ジェネリクスを使った関数の定義は、通常の関数定義と似ていますが、型引数を尖括弧< >
で指定する点が異なります。次の例では、型引数T
を使って、どんな型の値でも受け取れる関数を作成しています。
function identity<T>(arg: T): T {
return arg;
}
この例では、identity
関数は引数arg
として任意の型T
を受け取り、その型の値をそのまま返します。このように、ジェネリクスを使うことで、どの型にも対応できる汎用的な関数を定義できます。
7.2.2 ジェネリクスを使った関数の利用
ジェネリクスを使った関数を呼び出す際には、型推論により、TypeScriptが自動的に引数の型を判断してくれます。また、明示的に型を指定することも可能です。
let output1 = identity<string>("Hello"); // 明示的に型を指定
let output2 = identity(100); // 型推論による自動判定
output1
では、identity
関数に対してstring
型を明示的に指定していますが、output2
ではTypeScriptが100
という引数からnumber
型を推論しています。
7.3 ジェネリック型の複数利用
TypeScriptでは、ジェネリクスを使って複数の型引数を持つ関数やクラスを作成することができます。これにより、2つ以上の異なる型に対して柔軟に対応する関数やクラスを定義できます。
7.3.1 複数の型引数を持つ関数
以下の例では、ジェネリクスを使って2つの異なる型引数T
とU
を持つ関数を定義しています。この関数は、2つの異なる型の値を受け取り、それらを組み合わせたオブジェクトを返します。
function createPair<T, U>(first: T, second: U): { first: T; second: U } {
return { first, second };
}
let pair = createPair<string, number>("Hello", 42);
console.log(pair); // { first: 'Hello', second: 42 }
この関数createPair
では、T
型とU
型の2つの型引数を受け取り、それぞれの引数に対して型注釈を付けています。これにより、異なる型の値を安全に扱うことができます。
7.4 ジェネリクスを使ったクラスの定義
ジェネリクスは関数だけでなく、クラスにも適用できます。ジェネリクスを使ってクラスを定義すると、そのクラスが複数の異なる型に対して柔軟に機能するようになります。
7.4.1 ジェネリッククラスの基本
次に、ジェネリクスを使った基本的なクラス定義を見てみましょう。このクラスでは、任意の型を受け取るスタック(後入れ先出しのデータ構造)を定義しています。
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
let numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20
この例では、Stack
クラスがジェネリクスT
を使って、スタックに格納するアイテムの型を指定できるようになっています。型安全性を保ちながら、どの型のデータでも扱えるスタックを実装しています。
7.4.2 複数の型引数を持つクラス
ジェネリクスを使って、複数の型引数を持つクラスも作成できます。以下の例では、キーと値のペアを扱うジェネリックなクラスを定義しています。
class KeyValuePair<K, V> {
constructor(public key: K, public value: V) {}
display(): void {
console.log(`${this.key}: ${this.value}`);
}
}
let pair = new KeyValuePair<string, number>("age", 25);
pair.display(); // "age: 25"
このクラスKeyValuePair
では、K
とV
という2つの型引数を持ち、それぞれキーと値の型として使用しています。このように、複数の型引数を使うことで、汎用的なクラスを作成できます。
7.5 型制約(Constraints)
ジェネリクスは非常に柔軟ですが、時には型に対して特定の条件を課したい場合もあります。TypeScriptでは、**型制約(constraints)**を使って、ジェネリック型に対して「この型は必ず特定のプロパティやメソッドを持つべき」という条件を指定することができます。
7.5.1 型制約の基本
次の例では、ジェネリック型T
が必ずlength
プロパティを持っている必要があることを型制約で指定しています。これにより、文字列や配列のようにlength
プロパティを持つ型に対してのみ、この関数が使用可能になります。
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
logLength("Hello"); // 5
logLength([1, 2, 3]); // 3
// logLength(123); // エラー: number型にはlengthプロパティがない
この例では、T
はlength
プロパティを持つ型に限定されています。そのため、文字列や配列は許容されますが、数値はlength
プロパティを持たないため、エラーが発生します。
7.5.2 型制約とインターフェースの組み合わせ
型制約は、インターフェースと組み合わせて使うこともできます。ジェネリクスに対してインターフェースを適用することで、より詳細な型制約を行うことが可能です。
interface HasId {
id: number;
}
function printId<T extends HasId>(obj: T): void {
console.log(`ID: ${obj.id}`);
}
let user = { id: 101, name: "Alice" };
printId(user); // "ID: 101"
この例では、HasId
インターフェースを型制約として使っています。printId
関数は、id
プロパティを持つオブジェクトであれば受け取ることができます。
7.6 ジェネリック型のデフォルト値
TypeScriptでは、ジェネリクスに対してデフォルトの型を設定することも可能です。これにより、型引数が明示されなかった場合にデフォルトの型を適用することができます。
7.6.1 デフォルト型の指定
次の例では、ジェネリック型T
に対してstring
型をデフォルトとして指定しています。型引数が省略された場合、デフォルトのstring
型が使用されます。
function createItem<T = string>(value: T): T {
return value;
}
let item1 = createItem(100); // Tはnumber型
let item2 = createItem("Hello"); // Tはstring型
この例では、createItem
関数に対してT
のデフォルト型としてstring
を指定しています。item2
ではstring
型が自動的に適用されますが、item1
では明示的にnumber
型が推論されています。
7.7 実践的なジェネリクスの使い方
ジェネリクスは、さまざまな場面で非常に役立つ機能です。特に、型に依存しない汎用的な関数やクラスを作成する際に、その真価を発揮します。ここでは、ジェネリクスを活用した実践的な例をいくつか紹介します。
7.7.1 ジェネリックユーティリティ型
TypeScriptには、ジェネリクスを活用したユーティリティ型がいくつか用意されています。これにより、複雑な型操作をシンプルに行うことができます。例えば、Partial<T>
型は、すべてのプロパティがオプションになる型を作成します。
interface User {
id: number;
name: string;
age: number;
}
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
let currentUser: User = { id: 1, name: "Alice", age: 25 };
let updatedUser = updateUser(currentUser, { age: 26 });
console.log(updatedUser); // { id: 1, name: 'Alice', age: 26 }
この例では、Partial<User>
型を使って、User
型のプロパティをすべてオプションにしています。これにより、updateUser
関数で必要なプロパティのみ更新できるようになります。
まとめ
この章では、TypeScriptのジェネリクスについて詳しく解説しました。ジェネリクスを使うことで、型安全性を保ちながら柔軟で再利用可能なコードを作成できます。ジェネリック関数やクラスの使い方、型制約の設定、複数の型引数の利用、そして実践的なジェネリックユーティリティ型などを学びました。