TypeScript ジェネリクス

ジェネリクスは、コードの再利用性と柔軟性を高めるための強力なツールです。ジェネリクスを使うことで、型安全性を犠牲にすることなく、複数のデータ型を扱うことができるコードを書くことができます。

TypeScript ジェネリクスの実装

ジェネリクスを使用する一例として、恒等関数を見てみます。

function identity(a: number): number {
    return a;
}

この関数は受け取った値をそのまま返します。

それでは、引数が文字列や配列の場合はどうでしょうか?それぞれのデータ型に対して別々の関数を書くこともできますが、それでは繰り返しになり、効率が悪くなってしまいます。

その代わりに、ジェネリクスを使って恒等関数をより柔軟にすることができます。

function identity<T>(a: T): T {
    return a;
}

このバージョンの恒等関数では、関数名と括弧の間に型パラメータTを追加しています。これはTypeScriptに対して、この関数を汎用的なものにしたい、つまりどんなデータ型でも扱えるようにしたいということを伝えるものです。

型変数Tは、関数が呼び出されたときに渡される実際の型のプレースホルダとして機能します。例えば、identity(1)を呼び出すと、TypeScriptはTがnumberであると推論します。identity("hello world")と呼べば、TypeScriptはTがstringであると推論します。

TypeScriptが型を推論するため、identity関数を使う際に型のパラメータを渡す必要はありません。しかし、必要な時には型のパラメータを自由に設定することが可能です。

const result1 = identity(1); // 1
const result2 = identity('hello world'); // 'hello world'
const result3 = identity([1, 2, 3]); // [1, 2, 3]

// 型を指定
const result4 = identity<number>(10); // 10

TypeScript ジェネリクスの例

それではジェネリクスを使った他の例を見てみましょう。例えば、以下のようなUserというインターフェースがあるとします。

interface User {
    id: number;
    name: string;
    email: string;
}

Userオブジェクトの配列をある条件に基づいてフィルタリングする関数を考えます。

function filterUsers(users: User[], condition: (user: User) => boolean): User[] {
    return users.filter(condition);
}

この関数はUserオブジェクトに対しては非常に有効ですが、あまり柔軟ではありません。たとえばProductオブジェクトのような、異なる型のオブジェクトの配列にフィルタをかけたい場合はどうすれば良いでしょうか?

オブジェクトの種類ごとに別の関数を書くこともできますが、それでは繰り返しが多くなり、非効率的になってしまいます。ここでジェネリクスを用いて汎用的にしてみましょう。

function filter<T>(items: T[], condition: (item: T) => boolean): T[] {
    return items.filter(condition);
}

このバージョンの関数では、User型を汎用型Tに置き換えています。これにより、この関数は、条件関数がその型でも動作する限り、どのような型のオブジェクトでも動作するようになります。

filter関数を使って、Userオブジェクトの配列にフィルタをかけると、次のようになります。

const users: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
    { id: 3, name: 'Charlie', email: 'charlie@example.com' }
];

const filteredUsers = filter(users, user => user.name.startsWith('A'));

console.log(filteredUsers); // [{ "id": 1, "name": "Alice", "email": "alice@example.com" }]

同じ関数を使って、次のようにProductオブジェクトの配列にフィルタをかけることもできます。

interface Product {
    id: number;
    name: string;
    price: number;
}

const products: Product[] = [
    { id: 1, name: 'Apples', price: 0.5 },
    { id: 2, name: 'Bananas', price: 0.25 },
    { id: 3, name: 'Oranges', price: 0.75 }
];

const filteredProducts = filter(products, product => product.price > 0.5);

console.log(filteredProducts); // [{ "id": 3, "name": "Oranges", "price": 0.75 }]

TypeScript ジェネリクスの発展

それでは他の例も見てみましょう。例えば、以下のようなPersonというインターフェースがあるとしましょう。

interface Person {
    id: number;
    name: string;
    age: number;
}

Personオブジェクトの配列があり、あるプロパティに基づいてフィルタリングを実行したいと考えています。

function filterByProp<T, K extends keyof T>(items: T[], prop: K, value: T[K]): T[] {
    return items.filter(item => item[prop] === value);
}

この関数では、2つの型パラメータを使っています。Tは配列内のオブジェクトの型を表し、Kはフィルタリングしたいオブジェクトプロパティのキーを表します。extendsキーワードは、K型パラメータがT型の有効なキーであることを制約するために使用されています。これにより、フィルタリングしたいプロパティが配列内のオブジェクトに実際に存在することが保証されます。

これで、filterByProp関数を使用して、特定のプロパティに基づいてPersonオブジェクトの配列をフィルタリングすることができます。

const people: Person[] = [
    { id: 1, name: 'Alice', age: 25 },
    { id: 2, name: 'Bob', age: 30 },
    { id: 3, name: 'Charlie', age: 35 }
];

const filteredPeople = filterByProp(people, 'name', 'Alice');
const filteredPeople2 = filterByProp(people, 'age', 30);

console.log(filteredPeople); // [{ "id": 1, "name": "Alice", "age": 25 }]
console.log(filteredPeople2); // [{ "id": 2, "name": "Bob", "age": 30 }]

最初の例では、people配列を nameプロパティに基づいてフィルタリングし、値が'Alice'である人を探しています。2つ目の例では、people配列をageプロパティに基づいてフィルタリングし、値30を持つ人を探しています。

この記事を書いた人

著者の画像

Jeffry Alvarado

Ex-Facebook Engineer 大学ではコンピュータサイエンスを専攻し、在学中に複数のインターンシップを経験。コンピュータサイエンスが学習できるプラットフォームRecursionを創業し、CTOとしてカリキュラム作成、ソフトウェア開発を担当。


ツイート