TypeScriptにおけるパラメトリックポリモーフィズムの使い方と具体例

TypeScriptのジェネリクス機能を利用することで、パラメトリックポリモーフィズム(Parametric Polymorphism)が可能になります。この概念は、異なる型に対して同じコードを再利用できる柔軟性をもたらし、型安全性を保ちながらも汎用的な関数やクラスを作成することを可能にします。本記事では、TypeScriptにおけるパラメトリックポリモーフィズムの基本概念から、具体的な使用例、注意点までを解説し、実際の開発に役立つ実践的なアプローチを紹介します。

目次
  1. パラメトリックポリモーフィズムとは
    1. TypeScriptにおける重要性
  2. ジェネリクスを使った基本的な例
    1. ジェネリック関数の例
    2. ジェネリクスを使った関数の呼び出し
  3. 関数におけるジェネリクスの活用法
    1. 複数のジェネリック型パラメータを使った例
    2. ジェネリクスの実際の利用シーン
    3. 制約を持たせたジェネリクスの例
  4. クラスにおけるパラメトリックポリモーフィズム
    1. ジェネリッククラスの基本例
    2. ジェネリッククラスの使用例
    3. クラスメソッドにおけるジェネリクスの活用
    4. クラスに制約を持たせたジェネリクス
  5. インターフェースとジェネリクスの組み合わせ
    1. ジェネリックインターフェースの基本例
    2. ジェネリックインターフェースの使用例
    3. ジェネリックインターフェースとクラスの組み合わせ
    4. インターフェースに制約を持たせたジェネリクス
  6. 実際のプロジェクトでの応用例
    1. ジェネリクスを使ったAPIレスポンスの処理
    2. 汎用的なUIコンポーネントの作成
    3. データ構造の柔軟な管理
    4. まとめ
  7. トラブルシューティングと注意点
    1. 型推論が期待通りに動作しない場合
    2. 型制約の誤解
    3. ジェネリクスによる型の複雑化
    4. ユニオン型との互換性問題
    5. まとめ
  8. TypeScriptと他の言語との比較
    1. TypeScript vs. Javaにおけるジェネリクス
    2. TypeScript vs. C#におけるジェネリクス
    3. TypeScriptと他の動的型付け言語との違い
    4. 他の言語と比べたTypeScriptの強み
    5. まとめ
  9. 演習問題: ジェネリクスを使ったコードを書いてみよう
    1. 問題1: ジェネリクスを使ったスタック(Stack)クラスの作成
    2. 問題2: フィルター関数をジェネリクスで実装する
    3. まとめ
  10. まとめ

パラメトリックポリモーフィズムとは

パラメトリックポリモーフィズムは、特定の型に依存せずに、さまざまな型に対して同じコードを適用できるプログラミング技法です。TypeScriptでは、この技法をジェネリクスを通じて実現します。これにより、コードの再利用性を高め、異なるデータ型に対しても型安全な操作が可能となります。

TypeScriptにおける重要性

パラメトリックポリモーフィズムは、汎用的な機能を持つ関数やクラスを作成する際に特に有用です。例えば、配列のようなデータ構造を操作する場合、要素の型が異なっても同じ操作を行いたいことがよくあります。ジェネリクスを使うことで、こうしたニーズに応え、より安全で効率的なコードを書くことが可能になります。

ジェネリクスを使った基本的な例

TypeScriptにおいて、ジェネリクスは汎用的な関数やクラスを定義するための強力なツールです。ジェネリクスを使うことで、特定の型に依存せず、さまざまな型を扱える柔軟なコードを作成できます。

ジェネリック関数の例

ジェネリクスを用いた基本的な関数の例を示します。ここでは、どのような型の引数にも対応できる関数を作成します。

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

このidentity関数は、入力された型に関わらず、そのまま返す機能を持っています。ここで<T>は、関数内で使用する型を指定しており、呼び出し時に具体的な型が決まります。

ジェネリクスを使った関数の呼び出し

この関数を使う際に、型を明示的に指定することも、コンパイラに推測させることもできます。

let numberIdentity = identity<number>(42); // 明示的に型を指定
let stringIdentity = identity("Hello, TypeScript!"); // 型推論により自動的に推測

ジェネリクスにより、型の安全性を保ちながら柔軟にさまざまな型に対応できる関数を作成できます。これにより、同じロジックを再利用することで、冗長なコードを避けることができます。

関数におけるジェネリクスの活用法

ジェネリクスは関数内で、型を汎用的に扱う際に特に有効です。TypeScriptでは、関数に対してジェネリック型を導入することで、型安全なコードを書きながら、異なる型に対応する柔軟性を持たせることができます。

複数のジェネリック型パラメータを使った例

ジェネリック型パラメータは、1つだけではなく複数を使用することも可能です。これにより、異なる型同士の関係を保持しつつ、柔軟な関数を作ることができます。例えば、2つの異なる型の値を受け取り、それらをペアにする関数を考えてみましょう。

function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}

このpair関数は、2つの異なる型の引数を受け取り、それらをタプルとして返します。TUはそれぞれ異なる型を表し、呼び出し時に型が決定されます。

ジェネリクスの実際の利用シーン

例えば、数値と文字列を組み合わせる場面では、以下のように使用できます。

let result = pair<number, string>(42, "Answer");
console.log(result); // 出力: [42, "Answer"]

ジェネリクスを使うことで、異なる型の引数を扱う関数を型安全に作成でき、同じロジックを再利用することができます。また、型の組み合わせによって柔軟に機能を拡張することも可能です。

制約を持たせたジェネリクスの例

場合によっては、ジェネリック型に制約を設けることで、特定の型やその性質に限定した操作を行うことができます。例えば、渡された型がlengthプロパティを持つものに限定したい場合は、以下のように書けます。

interface Lengthwise {
    length: number;
}

function logLength<T extends Lengthwise>(arg: T): void {
    console.log(arg.length);
}

このlogLength関数は、lengthプロパティを持つオブジェクトであれば、どのような型でも受け付けます。

logLength({ length: 10, name: "Sample" }); // 出力: 10

このように制約を使うことで、特定の条件を満たした型に対してのみジェネリクスを適用することができ、さらに安全で信頼性の高いコードが実現します。

クラスにおけるパラメトリックポリモーフィズム

TypeScriptでは、クラスにもジェネリクスを導入することで、異なる型に対応する柔軟な設計が可能です。これにより、クラスのインスタンスごとに異なる型のデータを扱うことができ、コードの再利用性と型安全性が向上します。

ジェネリッククラスの基本例

ジェネリクスを使ったクラスは、定義時に汎用的な型を使用し、実際に使用する際にその型を指定します。以下の例では、Tというジェネリック型を持つクラスを定義し、さまざまな型の値を保持できるクラスを作成します。

class Box<T> {
    private contents: T;

    constructor(value: T) {
        this.contents = value;
    }

    getContents(): T {
        return this.contents;
    }

    setContents(value: T): void {
        this.contents = value;
    }
}

このBoxクラスは、T型のデータを格納するために設計されており、どの型にも対応できるようになっています。次に、このクラスを使う際に、具体的な型を指定する例を見てみましょう。

ジェネリッククラスの使用例

このクラスをインスタンス化するときには、保持するデータの型を指定します。以下の例では、number型とstring型のBoxをそれぞれ作成しています。

let numberBox = new Box<number>(123);
console.log(numberBox.getContents()); // 出力: 123

let stringBox = new Box<string>("TypeScript");
console.log(stringBox.getContents()); // 出力: "TypeScript"

このように、同じクラスでも異なる型を持つインスタンスを作成できるため、再利用性が高まります。

クラスメソッドにおけるジェネリクスの活用

クラスそのものだけでなく、クラスのメソッドにもジェネリクスを適用することができます。次に、クラスメソッドでジェネリクスを使用する例を示します。

class Pair<T, U> {
    private first: T;
    private second: U;

    constructor(first: T, second: U) {
        this.first = first;
        this.second = second;
    }

    swap(): [U, T] {
        return [this.second, this.first];
    }
}

このPairクラスは、2つの異なる型を受け取り、それらを保持します。また、swapメソッドは、2つの要素の順序を入れ替えたタプルを返します。

let pair = new Pair<number, string>(42, "Hello");
console.log(pair.swap()); // 出力: ["Hello", 42]

この例では、クラス自体がジェネリクスを使用しつつ、メソッド内でもジェネリクスの特性を活かして、柔軟な操作を行っています。

クラスに制約を持たせたジェネリクス

クラスのジェネリクスにも制約を持たせることで、特定のプロパティやメソッドを持つ型に限定することができます。次に、lengthプロパティを持つ型に対してジェネリクスを適用したクラスの例を示します。

class Container<T extends { length: number }> {
    private value: T;

    constructor(value: T) {
        this.value = value;
    }

    getLength(): number {
        return this.value.length;
    }
}

let stringContainer = new Container("TypeScript");
console.log(stringContainer.getLength()); // 出力: 10

この例では、ジェネリック型Tに対してlengthプロパティを持つ型に限定しており、stringのような型が適用可能です。

このように、ジェネリクスをクラスに導入することで、型安全なデータの操作が可能となり、柔軟で再利用性の高いクラス設計が実現します。

インターフェースとジェネリクスの組み合わせ

TypeScriptでは、インターフェースとジェネリクスを組み合わせることで、型安全性を維持しながら柔軟なデータ構造や動作を定義することが可能です。これにより、異なる型に対して共通の契約(インターフェース)を持たせつつ、特定の型情報を注入できるようになります。

ジェネリックインターフェースの基本例

ジェネリクスを使ったインターフェースは、異なる型に対応できる汎用的な定義を提供します。次の例では、Tというジェネリック型を受け取るインターフェースKeyValuePairを定義しています。

interface KeyValuePair<T, U> {
    key: T;
    value: U;
}

このKeyValuePairインターフェースは、keyvalueという2つのプロパティを持ち、それぞれ異なる型を設定することができます。このインターフェースを使用することで、異なる型のキーと値のペアを表現できます。

ジェネリックインターフェースの使用例

ジェネリックインターフェースは、具体的な型を注入することで利用します。次の例では、number型のキーとstring型の値を持つインスタンスを作成しています。

let item: KeyValuePair<number, string> = { key: 1, value: "TypeScript" };
console.log(item); // 出力: { key: 1, value: "TypeScript" }

また、別の型の組み合わせも簡単に扱うことができます。

let anotherItem: KeyValuePair<string, boolean> = { key: "isComplete", value: true };
console.log(anotherItem); // 出力: { key: "isComplete", value: true }

このように、インターフェースにジェネリクスを導入することで、異なる型に対応しつつ、型安全なコードを維持することが可能になります。

ジェネリックインターフェースとクラスの組み合わせ

ジェネリックインターフェースは、クラスと組み合わせることで、クラスが特定のインターフェース契約に従いながら、柔軟な型を扱えるように設計できます。次の例では、Storageというインターフェースを定義し、それを実装するクラスを作成します。

interface Storage<T> {
    addItem(item: T): void;
    getItem(index: number): T;
}

class ArrayStorage<T> implements Storage<T> {
    private items: T[] = [];

    addItem(item: T): void {
        this.items.push(item);
    }

    getItem(index: number): T {
        return this.items[index];
    }
}

このArrayStorageクラスは、任意の型Tのアイテムを配列に保存し、そのアイテムを取得できるようになっています。インターフェースStorage<T>は、T型のアイテムを扱うという契約をクラスに対して強制しています。

let stringStorage = new ArrayStorage<string>();
stringStorage.addItem("Hello");
console.log(stringStorage.getItem(0)); // 出力: "Hello"

let numberStorage = new ArrayStorage<number>();
numberStorage.addItem(42);
console.log(numberStorage.getItem(0)); // 出力: 42

このように、インターフェースとジェネリクスを組み合わせることで、クラスや関数が共通の構造に従いながら、型安全で汎用的なコードを提供することが可能です。

インターフェースに制約を持たせたジェネリクス

ジェネリックインターフェースにも型制約を加えることができます。制約を付けることで、特定の型に対してのみインターフェースを適用し、型の安全性をさらに高めることができます。

interface Lengthwise {
    length: number;
}

interface StorageWithLength<T extends Lengthwise> {
    addItem(item: T): void;
    getItem(index: number): T;
}

class ArrayStorageWithLength<T extends Lengthwise> implements StorageWithLength<T> {
    private items: T[] = [];

    addItem(item: T): void {
        this.items.push(item);
    }

    getItem(index: number): T {
        return this.items[index];
    }
}

この例では、T型にlengthプロパティを要求しています。これにより、lengthを持たない型が渡されることを防ぎ、型安全性をさらに強化しています。

インターフェースとジェネリクスを組み合わせることで、強力で柔軟な型システムを実現し、開発効率やコードのメンテナンス性を向上させることができます。

実際のプロジェクトでの応用例

パラメトリックポリモーフィズムを活用することで、TypeScriptを使用した実際のプロジェクトにおいて、型安全で再利用可能なコードを効率的に構築できます。特に、汎用的なデータ処理やUIコンポーネントの設計において、ジェネリクスを利用することで、異なる型に対応するコードを一つの実装で対応可能にすることができます。

ジェネリクスを使ったAPIレスポンスの処理

多くのプロジェクトでは、APIからのレスポンスを受け取り、そのデータを処理する必要があります。APIから返されるデータの構造は異なる場合がありますが、ジェネリクスを使用することで一貫した型安全なレスポンス処理が可能になります。

interface ApiResponse<T> {
    data: T;
    status: number;
    message: string;
}

function fetchData<T>(url: string): Promise<ApiResponse<T>> {
    return fetch(url)
        .then(response => response.json())
        .then(data => ({ data, status: response.status, message: "Success" }));
}

この関数fetchDataは、どのような型のデータも返すことができ、APIのレスポンス型を明示的に指定することが可能です。例えば、Userという型のデータを取得する場合、以下のように使います。

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

fetchData<User>('/api/user/1').then(response => {
    console.log(response.data.name); // 型安全にUser型のデータを操作
});

このようにジェネリクスを使うことで、APIから取得したデータが正しい型であることを保証し、型安全にデータを扱うことができます。

汎用的なUIコンポーネントの作成

フロントエンド開発では、UIコンポーネントを汎用化するためにジェネリクスが非常に有効です。例えば、リストアイテムを表示する汎用的なコンポーネントを作成し、異なる型のデータに対しても同じロジックを適用することができます。

interface ListItemProps<T> {
    items: T[];
    renderItem: (item: T) => JSX.Element;
}

function List<T>({ items, renderItem }: ListItemProps<T>): JSX.Element {
    return (
        <ul>
            {items.map((item, index) => (
                <li key={index}>{renderItem(item)}</li>
            ))}
        </ul>
    );
}

このListコンポーネントは、ジェネリクスTを使用して、異なる型のアイテムをリスト表示できるように設計されています。例えば、ユーザーのリストや商品リストなどを型安全に描画できます。

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

const products: Product[] = [
    { id: 1, name: 'Laptop', price: 1000 },
    { id: 2, name: 'Phone', price: 500 },
];

<List
    items={products}
    renderItem={(product) => <span>{product.name} - ${product.price}</span>}
/>

このように、ジェネリクスを使うことで、同じロジックをさまざまなデータ型に適用でき、UIコンポーネントの再利用性が向上します。

データ構造の柔軟な管理

パラメトリックポリモーフィズムを使うことで、複雑なデータ構造も柔軟に扱えるようになります。例えば、複数の異なるデータ型を持つ辞書を管理する場合にも、ジェネリクスを利用するとより簡潔で安全な実装が可能です。

interface Dictionary<T> {
    [key: string]: T;
}

function addEntry<T>(dictionary: Dictionary<T>, key: string, value: T): void {
    dictionary[key] = value;
}

const numberDict: Dictionary<number> = {};
addEntry(numberDict, "one", 1);
addEntry(numberDict, "two", 2);

console.log(numberDict); // 出力: { one: 1, two: 2 }

この例では、ジェネリクスを使用して、任意の型Tの辞書を操作できるようにしています。この柔軟性により、異なるデータ型を扱う辞書を1つのロジックで処理することが可能になります。

まとめ

実際のプロジェクトでは、ジェネリクスを使用して、APIデータの処理やUIコンポーネントの設計、データ構造の管理を行うことで、型安全性を保ちながら汎用的なコードを実現することができます。ジェネリクスを適切に活用することで、プロジェクト全体のコード品質が向上し、保守性も高まります。

トラブルシューティングと注意点

ジェネリクスを活用することで、型安全な汎用コードを作成できますが、いくつかのトラブルや注意点も存在します。これらの問題に事前に対応することで、効率的なコーディングが可能になります。ここでは、ジェネリクスを使う際に起こりやすい問題とその解決策を紹介します。

型推論が期待通りに動作しない場合

TypeScriptの強力な型推論機能は、通常、ジェネリクスを使う際にも適切に型を推論しますが、複雑な場合や特定の構造では推論がうまくいかないことがあります。例えば、複数のジェネリクス型パラメータを使う関数で、意図した型推論が行われないケースです。

function merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

このmerge関数は、2つのオブジェクトを結合しますが、TypeScriptが型を自動的に推論できない場合があります。こういったケースでは、明示的に型を指定することで解決します。

const mergedObject = merge<{ name: string }, { age: number }>({ name: "John" }, { age: 30 });

このように型を明示することで、期待通りに型推論が行われない場合でも、型安全なコードを維持できます。

型制約の誤解

ジェネリクスに型制約をつける場合、制約を適用した型のみが許可されるため、想定外のエラーが発生することがあります。例えば、以下の例では、T型にlengthプロパティを要求しています。

function logLength<T extends { length: number }>(arg: T): void {
    console.log(arg.length);
}

この関数を使う際、lengthを持たない型を渡すとコンパイルエラーが発生します。

logLength(10); // エラー: 'number'型には'length'プロパティが存在しません

このように、ジェネリクスに制約をつける際は、制約を十分に理解しておくことが重要です。制約が適用されることで、渡せる型が限定されますが、それによって型安全性が強化されることもメリットです。

ジェネリクスによる型の複雑化

ジェネリクスは強力ですが、複雑に使いすぎるとコードが読みづらくなり、メンテナンスが困難になる場合があります。例えば、複数のジェネリクス型パラメータを持つ関数やクラスが増えすぎると、開発者がその型を把握するのが難しくなることがあります。

function complexFunction<T extends U, U extends V, V>(arg: T): U {
    // かなり複雑な型関係
    return arg;
}

このような関数は、非常に強力である一方で、読みやすさや理解しやすさを犠牲にする可能性があります。ジェネリクスを使う際は、コードの可読性にも配慮し、必要に応じてコメントや補足説明を追加することが望ましいです。

ユニオン型との互換性問題

TypeScriptではユニオン型(A | B)をジェネリクスに適用する際、型の互換性に注意が必要です。ジェネリクスを使用する場合、ユニオン型が予期しない挙動を引き起こすことがあります。

function logValue<T>(value: T): void {
    console.log(value);
}

logValue<string | number>("Hello"); // OK
logValue<string | number>(42); // OK

ただし、ユニオン型を扱う場合、その型が持つプロパティやメソッドの操作には注意が必要です。次のように、ユニオン型を使うときに、TypeScriptがその型をすべて網羅できないことがあります。

function logLength<T extends { length: number }>(value: T | string): void {
    console.log(value.length); // エラー: 'string'型には'length'プロパティが存在しない可能性があります
}

このような場合、型の絞り込み(Type Narrowing)を使用して、明示的に型のチェックを行うことで問題を解決します。

function logLength<T extends { length: number }>(value: T | string): void {
    if (typeof value === "string" || 'length' in value) {
        console.log(value.length);
    }
}

まとめ

ジェネリクスを利用する際には、型推論の問題、型制約の理解不足、複雑な型構造、ユニオン型との互換性など、いくつかのトラブルに注意が必要です。これらの問題を適切に解決することで、型安全なコードを維持しつつ、柔軟かつ強力なジェネリクスの利点を最大限に活かすことができます。

TypeScriptと他の言語との比較

TypeScriptは、静的型付けを備えたJavaScriptのスーパーセットとして人気を集めていますが、他の型システムを持つ言語、特にJavaやC#などの主要なオブジェクト指向プログラミング言語と多くの共通点や相違点があります。ここでは、TypeScriptのジェネリクスを中心に、これらの言語との比較を行い、それぞれの利点や特徴を理解します。

TypeScript vs. Javaにおけるジェネリクス

JavaもTypeScriptと同様に、ジェネリクスをサポートしており、汎用的なクラスやメソッドを型安全に記述するために広く使用されています。ただし、Javaのジェネリクスは型消去(Type Erasure)のメカニズムに依存しており、実行時にはジェネリック型に関する情報が保持されません。

class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

一方、TypeScriptでは、コンパイル時に型チェックが行われますが、JavaScriptにトランスパイルされるため、実行時には型情報は保持されません。これにより、両言語とも実行時には型の恩恵が少なく、型の安全性は主に開発時に提供されるものです。

TypeScriptの利点は、より柔軟な型推論を持つ点です。例えば、TypeScriptは多くの場合において型を自動的に推論することができるため、開発者が明示的に型を指定する必要が少なくなります。

TypeScript vs. C#におけるジェネリクス

C#は、ジェネリクスを完全にサポートする言語の一つであり、TypeScriptに非常に似た構文を持っています。ただし、C#ではジェネリクスは実行時に型情報が保持されるため、ランタイムでも型安全性を提供します。以下は、C#におけるジェネリッククラスの例です。

class Box<T> {
    public T Value { get; set; }
}

C#のジェネリクスは、実行時に型情報が活用できるため、TypeScriptよりも強力な型システムを持つと言えます。また、C#の制約(Constraints)機能により、ジェネリクスに対してさらに詳細な型制約を設けることが可能です。

一方、TypeScriptでは、コンパイル時にのみ型の安全性が保証されますが、柔軟な型システムと非同期処理における型推論の容易さが利点となります。TypeScriptのジェネリクスは、フロントエンドとバックエンドの両方で動作するJavaScriptのトランスパイル先に最適化されています。

TypeScriptと他の動的型付け言語との違い

TypeScriptとPythonやRubyのような動的型付け言語を比較すると、最大の違いは静的型チェックです。動的型付け言語では、ジェネリクスに相当する機能が存在しないか、型安全性が実行時にしか確認されません。TypeScriptでは、開発中に型エラーを検出できるため、大規模プロジェクトでは保守性が向上します。

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

例えば、このTypeScriptのジェネリック関数identityは、どのような型の引数を取っても型安全に動作しますが、Pythonの同等のコードでは実行時にエラーが発生する可能性が高くなります。静的型チェックを持つTypeScriptは、より信頼性の高いコードを書くための手段として優れています。

他の言語と比べたTypeScriptの強み

  1. 柔軟な型推論: TypeScriptは、他の静的型付け言語に比べて型推論が強力です。多くの場面で型を明示的に指定する必要がなく、コードの記述量が減るため、生産性が向上します。
  2. JavaScriptエコシステムとの統合: TypeScriptはJavaScriptにトランスパイルされるため、既存のJavaScriptライブラリやツールをそのまま利用できます。JavaやC#とは異なり、フロントエンドやバックエンド、サーバーレスといった多様な環境で同じコードベースを使うことが可能です。
  3. 拡張性のある型システム: TypeScriptは、型アサーションや条件型など、他の言語にはない強力な型システムを提供しており、これにより開発者は複雑なデータ構造や動作を簡潔に表現できます。

まとめ

TypeScriptのジェネリクスは、JavaやC#など他の静的型付け言語と多くの共通点を持ちながら、柔軟で強力な型推論と、JavaScriptエコシステムとの深い統合性が際立っています。他の言語と比較すると、TypeScriptは特にフロントエンド開発において、型安全性と開発効率のバランスが取れた選択肢として優れています。

演習問題: ジェネリクスを使ったコードを書いてみよう

TypeScriptにおけるパラメトリックポリモーフィズムの理解を深めるため、実際にジェネリクスを活用したコードを書いてみましょう。ここでは、2つの演習問題を提示し、ジェネリクスを使った型安全なコードの実装方法を学びます。

問題1: ジェネリクスを使ったスタック(Stack)クラスの作成

まず、一般的なスタック(LIFO: Last In, First Out)データ構造を作成してみましょう。このスタックは、どの型のデータでも扱えるようにジェネリクスを使用します。以下の仕様に従ってクラスを実装してください。

要件:

  1. pushメソッドで要素をスタックに追加する。
  2. popメソッドでスタックの最後に追加された要素を削除して返す。
  3. peekメソッドでスタックの最後の要素を返すが、削除はしない。
  4. スタックのサイズを取得できるsizeプロパティを実装する。
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;
    }
}

// 使用例:
const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.peek()); // 出力: 20
console.log(numberStack.pop()); // 出力: 20
console.log(numberStack.size); // 出力: 1

挑戦:

  • スタックを任意の型でインスタンス化し、文字列、数値、またはオブジェクトを扱うスタックを試してください。
  • 型安全性を確認しながら、どのデータ型でも操作できることを確認してみましょう。

問題2: フィルター関数をジェネリクスで実装する

次に、配列内の要素をフィルタリングする汎用的な関数を作成します。この関数は、どのような型の配列でも動作するようにジェネリクスを使って実装します。指定された条件に従って配列の要素をフィルタリングし、新しい配列を返します。

要件:

  1. filterArray関数は、任意の型の配列と、条件を判定するコールバック関数を受け取る。
  2. コールバック関数は、配列の各要素に対してtrueまたはfalseを返し、trueであればその要素を保持し、falseなら削除する。
function filterArray<T>(arr: T[], callback: (item: T) => boolean): T[] {
    let result: T[] = [];
    for (let item of arr) {
        if (callback(item)) {
            result.push(item);
        }
    }
    return result;
}

// 使用例:
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filterArray(numbers, num => num % 2 === 0);
console.log(evenNumbers); // 出力: [2, 4]

const strings = ["apple", "banana", "cherry"];
const longStrings = filterArray(strings, str => str.length > 5);
console.log(longStrings); // 出力: ["banana", "cherry"]

挑戦:

  • filterArray関数を使って、オブジェクトの配列をフィルタリングしてみてください。
  • 条件に応じて、複数の異なるフィルタリングを試してみましょう。

まとめ

これらの演習問題を通じて、ジェネリクスを活用した型安全なデータ構造や関数を実装する経験を積むことができます。ジェネリクスを効果的に活用することで、TypeScriptで柔軟かつ再利用性の高いコードを書くスキルを身につけましょう。

まとめ

本記事では、TypeScriptにおけるパラメトリックポリモーフィズムの基本概念と実用的な活用方法について解説しました。ジェネリクスを利用することで、型安全性を保ちながら柔軟で再利用性の高いコードを簡単に作成できることが分かりました。関数やクラス、インターフェースにジェネリクスを適用することで、プロジェクト全体の保守性を向上させつつ、異なる型に対応する汎用的な実装が可能になります。

コメント

コメントする

目次
  1. パラメトリックポリモーフィズムとは
    1. TypeScriptにおける重要性
  2. ジェネリクスを使った基本的な例
    1. ジェネリック関数の例
    2. ジェネリクスを使った関数の呼び出し
  3. 関数におけるジェネリクスの活用法
    1. 複数のジェネリック型パラメータを使った例
    2. ジェネリクスの実際の利用シーン
    3. 制約を持たせたジェネリクスの例
  4. クラスにおけるパラメトリックポリモーフィズム
    1. ジェネリッククラスの基本例
    2. ジェネリッククラスの使用例
    3. クラスメソッドにおけるジェネリクスの活用
    4. クラスに制約を持たせたジェネリクス
  5. インターフェースとジェネリクスの組み合わせ
    1. ジェネリックインターフェースの基本例
    2. ジェネリックインターフェースの使用例
    3. ジェネリックインターフェースとクラスの組み合わせ
    4. インターフェースに制約を持たせたジェネリクス
  6. 実際のプロジェクトでの応用例
    1. ジェネリクスを使ったAPIレスポンスの処理
    2. 汎用的なUIコンポーネントの作成
    3. データ構造の柔軟な管理
    4. まとめ
  7. トラブルシューティングと注意点
    1. 型推論が期待通りに動作しない場合
    2. 型制約の誤解
    3. ジェネリクスによる型の複雑化
    4. ユニオン型との互換性問題
    5. まとめ
  8. TypeScriptと他の言語との比較
    1. TypeScript vs. Javaにおけるジェネリクス
    2. TypeScript vs. C#におけるジェネリクス
    3. TypeScriptと他の動的型付け言語との違い
    4. 他の言語と比べたTypeScriptの強み
    5. まとめ
  9. 演習問題: ジェネリクスを使ったコードを書いてみよう
    1. 問題1: ジェネリクスを使ったスタック(Stack)クラスの作成
    2. 問題2: フィルター関数をジェネリクスで実装する
    3. まとめ
  10. まとめ