前章では、TypeScriptにおける基本的な型推論について学びましたが、型安全性を維持しつつ、さらに柔軟で再利用可能なコードを作成するためには、TypeScriptの**ジェネリクス(Generics)**を理解し、応用することが重要です。
ジェネリクスは、汎用的な型を使うことで、異なる型に対応した関数やクラスを作成でき、型の安全性を損なうことなく、コードの再利用性を向上させる強力な機能です。この章では、ジェネリクスを使った高度な型定義や実践的な応用方法について詳しく解説します。
12.1 ジェネリクスの復習
ジェネリクスとは、特定の型に依存しない柔軟な型定義を可能にする仕組みです。ジェネリクスを使うことで、同じ関数やクラスが異なる型に対して再利用できるようになり、より柔軟で汎用的なプログラムを作成できます。
12.1.1 基本的なジェネリック関数
まず、簡単なジェネリック関数を復習しましょう。次の例では、ジェネリック型T
を使って、どのような型の値でも受け取れる汎用的な関数を定義しています。
function identity<T>(arg: T): T {
return arg;
}
この関数identity
は、引数の型に依存せず、T
というジェネリック型を使うことで、呼び出された際に適切な型が自動的に推論されます。例えば、identity<string>("hello")
のように呼び出すと、T
はstring
型として推論されます。
12.2 ジェネリクスを使った柔軟な関数
ジェネリクスを使うことで、単一の型に縛られない柔軟な関数を作成することができます。ここでは、ジェネリクスを活用したさまざまな関数を紹介します。
12.2.1 複数の型引数を持つ関数
ジェネリクスでは、複数の型引数を指定することができます。これにより、異なる型同士を組み合わせた汎用関数を作成できます。
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
この例では、merge
という関数が定義されており、2つのオブジェクトobj1
とobj2
を受け取り、それらを結合した新しいオブジェクトを返します。T
とU
という2つのジェネリック型を使って、それぞれ異なる型のオブジェクトを受け取ることができ、結合後のオブジェクトもその両方のプロパティを持つ型(T & U
)として返されます。
let person = merge({ name: "Alice" }, { age: 30 });
console.log(person.name); // "Alice"
console.log(person.age); // 30
このように、T
とU
という複数の型引数を使うことで、異なる型同士を安全に組み合わせることができる柔軟な関数を定義できます。
12.3 ジェネリクスを使ったクラス
ジェネリクスは、関数だけでなく、クラスにも適用できます。ジェネリッククラスを定義することで、さまざまな型のデータに対応した柔軟なクラスを作成することができます。
12.3.1 ジェネリッククラスの定義
次に、ジェネリクスを使ってスタック(LIFO:後入れ先出し)のデータ構造を定義してみましょう。このスタックは、どのような型のデータでも受け取れる汎用的なクラスです。
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];
}
get size(): number {
return this.items.length;
}
}
このStack
クラスは、ジェネリック型T
を使って定義されており、どの型のデータでも格納できるスタックを作成しています。スタックにデータを追加する際にはpush
メソッドを使い、最新のデータを取り出すにはpop
メソッドを使います。
let numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20
let stringStack = new Stack<string>();
stringStack.push("hello");
console.log(stringStack.peek()); // "hello"
この例では、number
型とstring
型のスタックを作成し、それぞれの型に対してジェネリッククラスが機能していることがわかります。
12.4 ジェネリクスとインターフェース
ジェネリクスは、インターフェースにも適用することができます。ジェネリックインターフェースを使うことで、汎用的なデータ構造やAPIを定義することができ、複数の型に対応した柔軟な設計が可能になります。
12.4.1 ジェネリックインターフェースの定義
次に、ジェネリックインターフェースの基本的な定義を見てみましょう。以下の例では、キーと値のペアを表すインターフェースを定義しています。
interface KeyValuePair<K, V> {
key: K;
value: V;
}
let kv1: KeyValuePair<string, number> = { key: "age", value: 30 };
let kv2: KeyValuePair<number, string> = { key: 1, value: "one" };
この例では、KeyValuePair
というジェネリックインターフェースが定義されています。K
はキーの型を、V
は値の型を表し、これにより、キーと値が異なる型のペアを表現することができます。
12.4.2 ジェネリックインターフェースの実装
インターフェースはクラスに実装することができます。ジェネリックインターフェースをクラスに適用することで、複数の型に対応したクラス設計が可能です。
interface Repository<T> {
getAll(): T[];
add(item: T): void;
}
class UserRepository implements Repository<string> {
private users: string[] = [];
getAll(): string[] {
return this.users;
}
add(user: string): void {
this.users.push(user);
}
}
let userRepo = new UserRepository();
userRepo.add("Alice");
userRepo.add("Bob");
console.log(userRepo.getAll()); // ["Alice", "Bob"]
この例では、Repository
というジェネリックインターフェースを使って、getAll
とadd
というメソッドを定義しています。UserRepository
クラスはstring
型を扱うリポジトリとして機能し、users
配列にstring
型のデータを管理しています。
12.5 型制約(Constraints)
ジェネリクスは非常に柔軟ですが、場合によっては、ジェネリック型に対して特定の型やプロパティを持つべき条件を課したいことがあります。こうした場合には、**型制約(constraints)**を使って、ジェネリック型に制限を付けることができます。
12.5.1 型制約の基本
型制約を使うと、ジェネリック型が特定の型に適合している場合にのみ、その型を受け取ることができます。以下の例では、T
がlength
プロパティを持つ型に制約を課しています。
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength("hello"); // OK: string型はlengthプロパティを持つ
logLength([1, 2, 3]); // OK: 配列もlengthプロパティを持つ
// logLength(123); // エラー: number型はlengthプロパティを持たない
この例では、T
は{ length: number }
という構造を持つ型に制限されています。そのため、文字列や配列など、length
プロパティを持つ型は受け入れられますが、数値のようにlength
プロパティを持たない型はエラーになります。
12.5.2 型制約とインターフェースの組み合わせ
インターフェースと型制約を組み合わせることで、より詳細な型の制約を設けることができます。
interface HasId {
id: number;
}
function printId<T extends HasId>(obj: T): void {
console.log(obj.id);
}
let user = { id: 123, name: "Alice" };
printId(user); // 123
この例では、HasId
というインターフェースを使って、T
がid
プロパティを持つ型に制約を付けています。そのため、id
プロパティを持つオブジェクトであれば、printId
関数に渡すことができます。
12.6 高度なジェネリクスの応用
ジェネリクスは、さらに高度な型操作にも応用できます。ここでは、ジェネリクスを使って実践的な型定義やデザインパターンを実現する方法を紹介します。
12.6.1 ジェネリックユーティリティ型の活用
TypeScriptには、ジェネリクスを使った標準のユーティリティ型がいくつか提供されています。これらを活用することで、型の操作や変換を簡単に行うことができます。
- Partial<T>: すべてのプロパティをオプションにします。
- Readonly<T>: すべてのプロパティを読み取り専用にします。
- Pick<T, K>: 特定のプロパティだけを抽出します。
- Omit<T, K>: 特定のプロパティを除外します。
これらのユーティリティ型は、ジェネリクスと組み合わせることで、柔軟な型定義を実現します。
type User = {
id: number;
name: string;
email: string;
};
type PartialUser = Partial<User>; // すべてのプロパティがオプションになる
type ReadonlyUser = Readonly<User>; // すべてのプロパティが読み取り専用になる
まとめ
この章では、TypeScriptにおけるジェネリクスの応用と実践的な使い方について学びました。ジェネリクスを使うことで、型安全性を保ちながらも、柔軟で再利用可能なコードを書くことができます。関数やクラス、インターフェースにジェネリクスを適用することで、さまざまな型に対応した汎用的なコードを実現できます。また、型制約を使ってジェネリクスの適用範囲を限定し、より安全なコードを設計することができました。