ジェネリクス(Generics)**は、TypeScriptにおいて、型を汎用的に扱うための非常に強力な機能です。ジェネリクスを使うことで、関数やクラス、インターフェースなどに対して、型を抽象化して再利用可能なコードを作成できます。この記事では、ジェネリクスの基本的な使い方から、応用的なパターンまでを解説します。
1. ジェネリクスの基本
ジェネリクスを使うと、関数やクラスに対して、呼び出し時に型を指定できるようになります。基本的なジェネリクスの使い方を見てみましょう。
function identity<T>(value: T): T {
return value;
}
型パラメータ T
この関数では、T
という型パラメータを使用しています。T
は、関数が呼び出される際に、特定の型として解決されます。この例では、関数に渡されたvalue
の型に応じて、T
がその型になります。これにより、どの型の値でもこの関数を再利用できます。
console.log(identity<string>("Hello")); // "Hello"
console.log(identity<number>(123)); // 123
identity
関数は、文字列でも数値でも動作し、指定された型に基づいて戻り値の型も自動的に決まります。
2. ジェネリクスの利点
ジェネリクスを使うことで、次のようなメリットがあります。
- 型安全: ジェネリクスは、型を柔軟に扱いつつも、型安全を維持します。型の不一致によるエラーを防ぎ、コードの信頼性を向上させます。
- 再利用性の向上: 同じロジックを異なる型に対して適用できるため、コードの再利用性が高まります。
- 柔軟性の向上: 型を固定せずに、関数やクラス、インターフェースを定義できるため、より汎用的なコードを記述できます。
3. 配列やオブジェクトに対するジェネリクス
ジェネリクスは、配列やオブジェクトに対しても使用できます。例えば、以下のようにジェネリック型を持つ配列を定義することができます。
function getFirstElement<T>(array: T[]): T {
return array[0];
}
console.log(getFirstElement([1, 2, 3])); // 1
console.log(getFirstElement(["a", "b", "c"])); // "a"
この例では、T[]
は任意の型T
の配列を表しており、関数はどんな型の配列にも適用できます。
オブジェクトの例
オブジェクトに対しても、ジェネリクスを使うことができます。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Alice", age: 25 };
console.log(getProperty(person, "name")); // "Alice"
console.log(getProperty(person, "age")); // 25
この例では、getProperty
関数がT
型のオブジェクトと、そのプロパティ名を指定するジェネリック型K
を受け取り、T
型オブジェクトの指定されたプロパティの値を返します。K extends keyof T
という制約により、K
はT
型オブジェクトのプロパティ名(keyof T
)であることが保証されます。
4. クラスにおけるジェネリクス
ジェネリクスは、クラスにも適用できます。例えば、データを扱うクラスをジェネリック型で定義することで、あらゆる型に対応する汎用的なクラスを作成できます。
class Box<T> {
private contents: T;
constructor(contents: T) {
this.contents = contents;
}
getContents(): T {
return this.contents;
}
}
const numberBox = new Box<number>(123);
console.log(numberBox.getContents()); // 123
const stringBox = new Box<string>("Hello");
console.log(stringBox.getContents()); // "Hello"
このBox
クラスでは、T
型の値を受け取って格納し、それを取り出すメソッドを提供しています。T
が型パラメータとして使われているため、インスタンスごとに異なる型を扱うことができます。
5. インターフェースにおけるジェネリクス
インターフェースにもジェネリクスを適用することができます。これにより、型が柔軟なインターフェースを定義できます。
interface Repository<T> {
getById(id: number): T;
getAll(): T[];
}
class User {
constructor(public id: number, public name: string) {}
}
class UserRepository implements Repository<User> {
private users: User[] = [
new User(1, "Alice"),
new User(2, "Bob"),
];
getById(id: number): User {
return this.users.find(user => user.id === id) as User;
}
getAll(): User[] {
return this.users;
}
}
const userRepository = new UserRepository();
console.log(userRepository.getById(1)); // User { id: 1, name: 'Alice' }
console.log(userRepository.getAll()); // [ User { id: 1, name: 'Alice' }, User { id: 2, name: 'Bob' } ]
この例では、Repository<T>
というインターフェースを定義し、それをUser
型に対して実装しています。T
がUser
に置き換わり、UserRepository
クラスではUser
型のデータを操作するメソッドが提供されます。
6. ジェネリック制約
ジェネリクスに制約を設けることで、特定の型に限定した汎用的な処理を行うことができます。たとえば、ある型にlength
プロパティが必要な場合、extends
キーワードを使って制約を付けることができます。
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength("Hello"); // 5
logLength([1, 2, 3]); // 3
// logLength(123); // エラー: 'number'型には'length'プロパティがない
この例では、T
型のパラメータはlength
プロパティを持つ型に制限されています。文字列や配列はlength
プロパティを持つため問題なく動作しますが、数値にはlength
がないためエラーになります。
7. 複数のジェネリック型パラメータ
複数のジェネリック型パラメータを使うことで、さらに柔軟な関数やクラスを定義できます。
function swap<T, U>(a: T, b: U): [U, T] {
return [b, a];
}
const result = swap("Hello", 123);
console.log(result); // [123, "Hello"]
この例では、T
とU
という2つのジェネリック型を使って、引数の型を入れ替える関数を定義しています。
8. ジェネリクスのデフォルト型
TypeScriptでは、ジェネリック型にデフォルトの型を設定することも可能です。これにより、ジェネリック型の明示が不要な場合でも、デフォルトの型が適用されます。
function createPair<T = string, U = number>(a: T, b: U): [T, U] {
return [a, b];
}
console.log(createPair("Hello", 42)); // ["Hello", 42]
console.log(createPair(10, true)); // [10, true]
console.log(createPair("Default")); // ["Default", 0] (Uはデフォルトでnumber)
この例では、T
はデフォルトでstring
、U
はデフォルトでnumber
として定義されています。
まとめ
ジェネリクスは、TypeScriptの型システムをさらに強化し、柔軟で再利用可能なコードを作成するための非常に有用な機能です。関数やクラス、インターフェースに対してジェネリクスを適用することで、型を抽象化しつつ、型安全を維持できます。ジェネリクスの基本を理解し、制約や複数の型パラメータ、デフォルト型などの応用を活用することで、より効率的な開発が可能になります。