見出し画像

#032 TypeScript(16):ジェネリクスの基本的な概念

ジェネリクスとは?

ジェネリクスは、コードの再利用性を高め、型安全性を保ちながら柔軟性を提供する強力な機能です。簡単に言えば、「型」を変数のように扱うことができる機能です。

Claudeに聞いた

この図は、TypeScriptのジェネリクスの基本的な概念を視覚化しています。以下に各部分の説明をします:

  1. ジェネリクス関数の例(緑色の箱):

    • identity<T> 関数は、どんな型の引数も受け取り、同じ型で返します。

    • <T> は型変数で、実際の使用時に具体的な型に置き換わります。

  2. 使用例(青色と黄色の箱):

    • 同じ identity 関数を数値と文字列で使用しています。

    • TypeScriptは、渡された引数の型に基づいて T を自動的に推論します。

  3. ジェネリクスの利点(下部のテキスト):

    • 型の再利用性、型安全性、コードの簡潔さを強調しています。

ジェネリクスの基本的な考え方:

  1. 型を変数のように扱う:

    • ジェネリクスを使うと、関数やクラスを定義する時点では具体的な型を指定せず、使用時に型を決定できます。

  2. 柔軟性と型安全性の両立:

    • 様々な型に対応できる柔軟な関数やクラスを作れます。

    • 同時に、TypeScriptの型チェックも維持されます。

  3. コードの再利用:

    • 同じロジックを異なる型に適用できるため、重複コードを減らせます。

ソースコード

// identity 関数は引数の型をそのまま返します。
// <T> はジェネリクスの型パラメータです。
// これにより、引数と戻り値の型を動的に決定できます。
function identity<T>(arg: T): T {
    return arg;
}

// ジェネリクスの型パラメータを明示的に指定して呼び出す例
let output1 = identity<string>("myString"); // output1 の型は string
let output2 = identity<number>(100); // output2 の型は number

// コンソールに出力
console.log("output1:", output1); // 出力: myString
console.log("output2:", output2); // 出力: 100

// GenericIdentityFn インターフェースはジェネリックな関数型を定義します。
// <T>(arg: T) => T は、引数と戻り値の型が同じであることを示します。
interface GenericIdentityFn {
    <T>(arg: T): T;
}

// identity2 関数もジェネリクスを使用しています。
function identity2<T>(arg: T): T {
    return arg;
}

// myIdentity は GenericIdentityFn 型の変数です。
// つまり、ジェネリックな identity2 関数を代入できます。
let myIdentity: GenericIdentityFn = identity2;

// myIdentity 関数を使って値を取得
let output3 = myIdentity<string>("hello");
let output4 = myIdentity<number>(200);

// コンソールに出力
console.log("output3:", output3); // 出力: hello
console.log("output4:", output4); // 出力: 200

// GenericNumber クラスは、ジェネリクスを使用して型パラメータ T を持ちます。
// zeroValue プロパティと add メソッドの型は T に依存します。
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

// myGenericNumber は GenericNumber<number> 型のインスタンスです。
// つまり、T は number 型に固定されています。
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0; // zeroValue の型は number
myGenericNumber.add = function(x, y) { return x + y; }; // add メソッドの引数と戻り値の型も number

// コンソールに出力
console.log("myGenericNumber.zeroValue:", myGenericNumber.zeroValue); // 出力: 0
console.log("myGenericNumber.add(10, 20):", myGenericNumber.add(10, 20)); // 出力: 30

ジェネリクスをオブジェクトで使用する

ジェネリクスを使用することで、さまざまな型で動作するオブジェクトを作成することができます。以下は、ジェネリクスをオブジェクトで使用するいくつかのパターンを示します。

01:ジェネリックインターフェース

ジェネリックインターフェースを使うことで、オブジェクトのプロパティの型を動的に定義できます。

// ジェネリックインターフェースの定義
interface GenericObject<T> {
    id: number;
    value: T;
}

// string 型を使用する場合
const stringObject: GenericObject<string> = {
    id: 1,
    value: "Hello"
};

// number 型を使用する場合
const numberObject: GenericObject<number> = {
    id: 2,
    value: 42
};

// コンソールに出力
console.log("stringObject:", stringObject); // 出力: { id: 1, value: 'Hello' }
console.log("numberObject:", numberObject); // 出力: { id: 2, value: 42 }

02:ジェネリック関数を使ったオブジェクト操作

ジェネリック関数を使って、オブジェクトのプロパティにアクセスする例です。

// ジェネリック関数の定義
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

// オブジェクトの定義
const person = {
    name: "Alice",
    age: 30
};

// プロパティを取得する関数を呼び出す
const name = getProperty(person, "name"); // name の型は string
const age = getProperty(person, "age"); // age の型は number

// コンソールに出力
console.log("name:", name); // 出力: Alice
console.log("age:", age); // 出力: 30

ジェネリック関数 getProperty は、オブジェクトから指定されたキーの値を取得するために使用されます。この関数の定義を詳しく見ていきましょう。

パラメータと型パラメータの説明

  1. ジェネリック型パラメータ T:

    • T は、任意のオブジェクトの型を表します。

    • 関数が呼び出されるときに具体的な型が決まります。

  2. ジェネリック型パラメータ K:

    • K は、T のキーのいずれかを表します。

    • K extends keyof T は、K が T のプロパティ名のいずれかであることを保証します。

    • keyof T は、型 T のすべてのプロパティ名(キー)をユニオン型として取得します。

関数の引数

  • obj: T: 型 T のオブジェクトを受け取ります。

  • key: K: 型 K のキーを受け取ります。K は T のプロパティ名のいずれかです。

関数の戻り値の型

  • T[K]: obj のプロパティ key の値の型を表します。これは、指定されたキーに対応する値の型です。

03:ジェネリッククラスを使ったオブジェクト操作

ジェネリッククラスを使って、オブジェクトのプロパティにアクセスする方法です。

// ジェネリッククラスの定義
class KeyValueStore<T> {
    private store: { [key: string]: T } = {};

    setItem(key: string, value: T): void {
        this.store[key] = value;
    }

    getItem(key: string): T | undefined {
        return this.store[key];
    }
}

// string 型の値を持つストア
const stringStore = new KeyValueStore<string>();
stringStore.setItem("name", "Alice");
console.log("name:", stringStore.getItem("name")); // 出力: Alice

// number 型の値を持つストア
const numberStore = new KeyValueStore<number>();
numberStore.setItem("age", 30);
console.log("age:", numberStore.getItem("age")); // 出力: 30

ジェネリッククラスの定義

  • KeyValueStore<T> はジェネリッククラスであり、<T> は型パラメータを表します。

  • T はクラスが扱うデータの型を動的に決定するために使用されます。

プロパティ store

  • store プロパティは、キーが文字列で値が型 T であるオブジェクトを格納します。

  • { [key: string]: T } は、プロパティ名が文字列で、そのプロパティの値が型 T であることを示すインデックスシグネチャです。

インデックスシグネチャは、オブジェクトのプロパティ名(キー)とプロパティの値の型を定義する方法です。これにより、動的にプロパティが追加されることが多いオブジェクトの型を安全に定義できます。TypeScriptでは、インデックスシグネチャを使って柔軟で型安全なオブジェクトを作成できます。

  • private 修飾子は、store プロパティがクラスの外部からアクセスできないことを示します。

メソッド setItem

  • setItem メソッドは、指定されたキーと値を store に格納します。

  • 引数 key は文字列型、value は型 T です。

  • メソッドの戻り値の型は void であり、値を返しません。

メソッド getItem

  • getItem メソッドは、指定されたキーに対応する値を store から取得します。

  • 引数 key は文字列型です。

  • 戻り値の型は T | undefined であり、キーに対応する値が存在しない場合は undefined を返します。

終わりに

少し長くなったので、ここで一旦お話をクローズさせて頂きます。
自分自身の学習記録帳のため、乱雑な文章になっている可能性があります。
申し訳ございません。

最後までお読みいただき、ありがとうございます。
私も皆さんのnoteを拝見させて頂いて、多くの気づきを得ています。
本当に感謝🙏です。
今後ともよろしくお願いします。

この記事が気に入ったらサポートをしてみませんか?