TypeScriptでのジェネリクスを使った関数オーバーロードの実装方法と応用例

TypeScriptでは、関数オーバーロードとジェネリクスを組み合わせることで、より柔軟かつ安全な関数を実装できます。関数オーバーロードは、同じ関数名で異なるパラメータの型や数に対応する機能を提供し、複数の異なる操作を1つの関数で扱うことを可能にします。一方、ジェネリクスは型をパラメータ化し、再利用性と型の安全性を高めます。本記事では、これら2つの機能を組み合わせて、複数の型やパラメータに対応できる効率的な関数を実装する方法について解説します。

目次

TypeScriptにおける関数オーバーロードとは

TypeScriptにおける関数オーバーロードは、同じ名前の関数を複数定義し、異なる型や引数の数に応じて異なる動作をさせる手法です。これにより、異なるデータ型やパラメータの組み合わせに対応する柔軟な関数を作成できます。関数オーバーロードでは、関数の宣言部分で複数のシグネチャを定義し、実際の関数定義は1つの実装にまとめます。

例えば、文字列を結合する関数と数値を加算する関数を同一の名前で提供する場合、以下のように関数オーバーロードを使用します。

function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
function combine(a: any, b: any): any {
    return a + b;
}

このように、TypeScriptは関数シグネチャに基づいて適切な型のオーバーロードを選択し、型安全に処理を行います。

ジェネリクスの基本概念

ジェネリクス(Generics)は、TypeScriptにおいて型をパラメータ化する機能です。ジェネリクスを使用することで、特定の型に依存せずに再利用可能な関数、クラス、インターフェースを作成することができます。これにより、複数の異なる型に対して同じロジックを適用することができ、かつ型安全性を保ちながら柔軟なコードが実現できます。

ジェネリクスを使う典型的な例として、配列から最初の要素を取得する関数を考えます。通常、引数に特定の型を指定すると、他の型に対応できなくなりますが、ジェネリクスを使えば柔軟に対応できます。

function firstElement<T>(arr: T[]): T {
    return arr[0];
}

この例では、Tがジェネリック型として宣言されており、呼び出し時に任意の型を指定できます。例えば、string[]number[]など、異なる型の配列に対しても型安全に同じ関数を適用できるようになります。

ジェネリクスを活用することで、コードの再利用性が向上し、型チェックが行われるため、開発者にとって安全で効率的なコーディングが可能になります。

ジェネリクスを使った関数オーバーロードの構造

TypeScriptでは、ジェネリクスを使った関数オーバーロードを利用することで、さらに柔軟な関数設計が可能になります。通常の関数オーバーロードは複数の型や引数の組み合わせに対応しますが、ジェネリクスを併用すると、オーバーロードの対象となる型を動的に決定しながらも、型安全性を保持できます。

ジェネリクスを用いた関数オーバーロードの基本的な構造は以下のようになります。

function merge<T>(a: T[], b: T[]): T[];
function merge<T, U>(a: T[], b: U[]): (T | U)[];
function merge(a: any[], b: any[]): any[] {
    return a.concat(b);
}

ここでは、merge関数が異なるシグネチャを持っています。最初のオーバーロードは、同じ型の要素を持つ2つの配列を結合します。一方、2つ目のオーバーロードでは、異なる型の配列を結合し、結果としてユニオン型の配列を返します。

関数本体の実装部分では、すべてのオーバーロードに共通する処理を行い、型チェックはオーバーロードされたシグネチャによって自動的に行われます。このように、ジェネリクスとオーバーロードを組み合わせることで、型に依存せず多様なケースに対応できる関数を作成することができます。

コード例:異なる型のデータを結合する

例えば、文字列配列と数値配列を結合する場合、ジェネリクスとオーバーロードを活用すると以下のように動作します。

const mergedArray = merge([1, 2], ['a', 'b']);
console.log(mergedArray); // 結果: [1, 2, 'a', 'b']

このように、ジェネリクスと関数オーバーロードの組み合わせは、汎用的かつ型安全な関数を実装するのに非常に有効です。

複数の型に対応する関数の設計

ジェネリクスを使った関数オーバーロードの最大の利点は、異なる型に対しても1つの関数で柔軟に対応できることです。これにより、型ごとに別々の関数を用意する必要がなくなり、コードの可読性やメンテナンス性が大幅に向上します。複数の型に対応する関数を設計する際には、ジェネリクスとオーバーロードの組み合わせが強力な武器となります。

関数オーバーロードを用いた複数の型対応

関数オーバーロードを使うことで、異なる型の引数を持つ関数を設計できます。たとえば、数値や文字列、さらには複合型のデータに対応する関数を設計する場合、次のようにジェネリクスを活用します。

function display<T>(value: T): string;
function display<T, U>(value: T, label: U): string;
function display(value: any, label?: any): string {
    if (label) {
        return `Label: ${label}, Value: ${value}`;
    }
    return `Value: ${value}`;
}

この例では、display関数が1つの引数または2つの引数を受け取ることができ、それぞれの型が異なっても型安全に動作します。TUというジェネリック型パラメータを使うことで、あらゆる型に対応可能な関数を作成できています。

実装例:異なるデータ型への対応

例えば、次のような使い方が可能です。

console.log(display(100)); // "Value: 100"
console.log(display('Hello', 'Greeting')); // "Label: Greeting, Value: Hello"

このように、ジェネリクスを使って複数の型に対応する関数を設計すると、どのような型の引数が渡されても、型安全にデータを処理できる柔軟な設計が可能です。

型に応じた異なる処理の実装

複数の型に対応する関数では、引数の型に応じて異なる処理を実装することもできます。例えば、数値の場合と文字列の場合で別々の処理を行いたい場合、型チェックを実装することが可能です。

function processValue<T>(value: T): string {
    if (typeof value === 'number') {
        return `Number: ${value * 2}`;
    } else if (typeof value === 'string') {
        return `String: ${value.toUpperCase()}`;
    } else {
        return 'Unknown type';
    }
}

console.log(processValue(42)); // "Number: 84"
console.log(processValue('typescript')); // "String: TYPESCRIPT"

このように、ジェネリクスを使った関数設計では、引数の型に応じた処理を効率よく実装でき、幅広いデータ型に対応する柔軟なコードを作成できます。

ジェネリクスとユニオン型の違い

TypeScriptでは、ジェネリクスとユニオン型はどちらも複数の型を扱うために利用されますが、これらは異なる役割と使い方を持っています。ジェネリクスは「特定の型をパラメータとして受け取る」仕組みであり、ユニオン型は「複数の型のどれか1つ」を示すものです。ここでは、ジェネリクスとユニオン型の違いと、それぞれの使いどころについて解説します。

ジェネリクスとは

ジェネリクスは、関数やクラスなどにおいて、型を後から決定するためのパラメータ化された型を指します。これにより、再利用可能で柔軟なコードが書けます。ジェネリクスでは、特定の型に依存せずにさまざまな型に対応でき、型の安全性を保つことができます。

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

console.log(identity<string>('Hello')); // "Hello"
console.log(identity<number>(10)); // 10

この例では、ジェネリクス<T>を使うことで、identity関数が異なる型に対しても一貫して型安全に処理を行えます。

ユニオン型とは

ユニオン型は、複数の型のうちどれか1つが許容されることを示します。これにより、1つの変数に対して複数の型を受け取ることができますが、関数内部で扱う際には、それぞれの型に応じた処理を自分で明示的に実装する必要があります。

function printValue(value: string | number): void {
    if (typeof value === 'string') {
        console.log(`String: ${value}`);
    } else {
        console.log(`Number: ${value}`);
    }
}

printValue('Hello'); // "String: Hello"
printValue(10); // "Number: 10"

この場合、stringnumberのどちらかの型を受け取ることができますが、どちらの型なのかを関数内で明示的に判別する必要があります。

ジェネリクスとユニオン型の違い

  • 柔軟性: ジェネリクスは「どの型でもいいが、一貫性を持って同じ型を使う」場合に適しています。一方、ユニオン型は「複数の異なる型のどれか一つ」を許容する際に使われます。
  • 型の一致性: ジェネリクスでは、1つの関数呼び出しの中で同じ型が使われます。例えば、あるジェネリック関数が引数にT型を取ると、その戻り値もT型になります。ユニオン型は、関数が異なる型を許容する場合に使われ、引数が複数の型を持つことが前提です。
  • コードの可読性: ジェネリクスを使用すると、コードの再利用性や可読性が向上します。ユニオン型を使うと、関数内部で型チェックや型キャストが必要になることが多く、処理が複雑になる可能性があります。

使いどころの違い

  • ジェネリクス: 同じ型での一貫した処理を行いたい場合や、関数の戻り値が引数と同じ型であることを保証したい場合に最適です。たとえば、配列操作やコレクションの処理などが挙げられます。
  • ユニオン型: 異なる型のデータを扱う必要がある場合、特定の操作に対して柔軟性を持たせたい場合に適しています。たとえば、数値や文字列を処理する関数や、異なる型のオブジェクトを扱う場合に有効です。

ジェネリクスとユニオン型の違いを理解することで、状況に応じた最適な設計ができ、より安全かつ効率的なTypeScriptのコードを書くことができます。

高度なジェネリクスの使用例

TypeScriptのジェネリクスは、シンプルな型のパラメータ化だけでなく、より高度なケースにも対応できます。特に、ジェネリクスを活用することで、柔軟かつ再利用性の高いコードを記述できるようになります。ここでは、ジェネリクスを使った高度な使用例をいくつか紹介します。

複数のジェネリック型パラメータの使用

1つのジェネリック型に限らず、複数の型パラメータを使用することが可能です。これにより、関数やクラスが複数の異なる型に依存する場合でも、ジェネリクスを使って効率的に処理できます。

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

const obj1 = { name: "John" };
const obj2 = { age: 30 };

const mergedObj = mergeObjects(obj1, obj2);
console.log(mergedObj); // { name: "John", age: 30 }

この例では、TUの2つのジェネリック型を使い、2つのオブジェクトを結合して新しいオブジェクトを生成しています。この関数はどんな型のオブジェクトでも受け入れることができ、型安全に2つのオブジェクトを合成できます。

ジェネリクスを使ったクラスの実装

ジェネリクスは関数だけでなく、クラスでも使うことができます。ジェネリッククラスを使うことで、特定の型に依存しない再利用可能なクラスを作成できます。

class KeyValuePair<K, V> {
    constructor(public key: K, public value: V) {}

    display(): void {
        console.log(`Key: ${this.key}, Value: ${this.value}`);
    }
}

const pair = new KeyValuePair<number, string>(1, "TypeScript");
pair.display(); // Key: 1, Value: TypeScript

ここでは、KVという2つのジェネリック型パラメータを持つクラスKeyValuePairを定義しています。このクラスは、どのような型のキーと値のペアでも扱うことができ、柔軟なデータ管理が可能です。

条件付き型 (Conditional Types) とジェネリクス

TypeScriptでは、条件付き型(Conditional Types)を使って、ジェネリクスの型に応じて異なる型を返すような仕組みを作成できます。これにより、ジェネリクスの動作をさらに高度に制御することが可能です。

type IsArray<T> = T extends any[] ? "Array" : "Not Array";

const test1: IsArray<number[]> = "Array";   // OK
const test2: IsArray<number> = "Not Array"; // OK

この例では、Tが配列かどうかを判定し、結果として異なる型を返しています。ジェネリクスと条件付き型を組み合わせることで、非常に強力な型システムが構築できます。

ジェネリクスとマップ型 (Mapped Types)

ジェネリクスは、マップ型(Mapped Types)とも組み合わせることで、オブジェクトの型を柔軟に変形することができます。例えば、既存のオブジェクトのプロパティをオプショナルにする場合、次のようなジェネリック型を定義できます。

type Optional<T> = {
    [P in keyof T]?: T[P];
};

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

const optionalPerson: Optional<Person> = {
    name: "Alice"
};

このOptional型は、Tのすべてのプロパティをオプショナルにするジェネリックマップ型です。マップ型を使うことで、オブジェクトのプロパティに対して動的に型操作を行うことが可能です。

ジェネリクスの制約を超えた柔軟なコード設計

ジェネリクスを使った関数やクラスは、型安全性を保ちながら、再利用性や柔軟性を高めます。特に、複数のジェネリックパラメータや条件付き型、マップ型などの高度な機能を駆使することで、複雑なシステムやライブラリでも柔軟かつ効率的なコード設計が可能になります。

ジェネリクスを使いこなすことで、TypeScriptの型システムをフル活用した高度なプログラミングが実現できます。実際のプロジェクトでこれらの技術を応用することで、よりメンテナンス性の高い、強力なコードを書くことができるでしょう。

型の制約を設けたジェネリクスの利用

ジェネリクスは非常に柔軟ですが、時にはその柔軟性を制限することで、より安全かつ目的に合った型を扱うことが求められます。TypeScriptでは、ジェネリック型に「制約」を設けることで、特定のプロパティやメソッドを持つ型にのみ適用できるようにすることができます。この制約は、型の安全性を高めつつ、必要な機能だけを使うことを可能にします。

型制約の基本概念

ジェネリクスに制約を追加する場合、extendsキーワードを使用します。これにより、指定した型やインターフェースを継承した型にのみジェネリック型を適用できるようになります。例えば、あるオブジェクトがlengthプロパティを持っていることを要求したい場合、次のように制約を設けることができます。

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

logLength("Hello"); // 5
logLength([1, 2, 3]); // 3

この例では、ジェネリック型Tは、lengthプロパティを持つ型に制限されています。そのため、stringArrayなど、lengthプロパティを持つ型に対してはこの関数が適用できますが、numberなどlengthを持たない型には適用できません。

複数の型制約を設ける

場合によっては、複数の型制約を同時に設定したいこともあります。そのような場合、&(交差型)を使って、複数の型に共通する制約を設けることができます。

interface HasName {
    name: string;
}

interface HasAge {
    age: number;
}

function showPersonInfo<T extends HasName & HasAge>(person: T): void {
    console.log(`Name: ${person.name}, Age: ${person.age}`);
}

const person = { name: "John", age: 30, profession: "Engineer" };
showPersonInfo(person); // Name: John, Age: 30

この例では、HasNameHasAgeという2つのインターフェースを同時に満たす型に制約を設けています。結果として、nameageの両方を持つオブジェクトにのみ関数を適用でき、型の安全性を保ちながら複数の制約を適用できます。

クラスに対するジェネリクスの制約

クラスに対してもジェネリック型を使用し、特定のインターフェースを継承するクラスのみを許容することが可能です。これにより、クラスのインスタンスが指定した制約を満たすことを保証できます。

class Animal {
    constructor(public name: string) {}
}

class Dog extends Animal {
    bark(): void {
        console.log("Woof!");
    }
}

class Cat extends Animal {
    meow(): void {
        console.log("Meow!");
    }
}

function createAnimal<T extends Animal>(animalClass: new (name: string) => T, name: string): T {
    return new animalClass(name);
}

const myDog = createAnimal(Dog, "Buddy");
console.log(myDog.name); // "Buddy"
myDog.bark(); // "Woof!"

この例では、Animalクラスを継承したクラスに対してのみジェネリクスを適用しています。この制約により、Animalを基にしたクラス(DogCatなど)だけが関数の引数として許容されます。

型制約の利点

型制約を設けることで、次のような利点があります。

  1. 型の安全性が向上: 制約を設定することで、コードが予期しない型に対して動作しないことを保証できます。これにより、実行時のエラーを防ぎ、型に基づいた開発が進めやすくなります。
  2. コードの可読性向上: 制約を使うことで、関数やクラスがどの型に対して使われるのかが明確になり、コードの意図が伝わりやすくなります。
  3. 柔軟性の維持: 制約を設けても、ジェネリクスの柔軟性は保たれます。特定の型だけでなく、より汎用的な型に対しても機能を提供できるため、再利用性が高まります。

ジェネリクスに型制約を設けることで、柔軟性と型安全性を両立させたコードを作成でき、TypeScriptの型システムをより効果的に活用できるようになります。これにより、信頼性の高いプログラムを設計することが可能になります。

関数オーバーロードと型推論の連携

TypeScriptでは、関数オーバーロードと型推論が密接に連携して動作し、開発者が明示的に型を指定しなくても適切な型を自動的に推論する機能を提供します。これにより、オーバーロードされた関数が柔軟に動作し、直感的なコード記述が可能となります。型推論を活用することで、開発効率の向上とコードの可読性向上が期待できます。

型推論とは

型推論とは、TypeScriptがコードの文脈に基づいて型を自動的に決定する仕組みです。これにより、開発者がすべての型を明示的に指定する必要がなく、TypeScriptのコンパイラが適切な型を推測します。特に、関数オーバーロードとジェネリクスを組み合わせた際、型推論が非常に有効に働きます。

関数オーバーロードでの型推論の使用例

以下の例は、関数オーバーロードを使用して異なる型の引数を受け取り、型推論によって自動的に適切な型が推測される例です。

function getValue(value: string): string;
function getValue(value: number): number;
function getValue(value: any): any {
    return value;
}

const result1 = getValue("Hello");
const result2 = getValue(42);

このコードでは、getValue関数が2つの異なるシグネチャを持ち、それぞれの型(stringまたはnumber)に応じて返り値の型が自動的に推論されます。result1にはstring型、result2にはnumber型が割り当てられ、開発者は型を明示することなく型推論を活用できます。

ジェネリクスと型推論の連携

ジェネリクスを用いた関数でも、型推論が大いに役立ちます。以下の例では、ジェネリクスを使った関数identityに対して、型推論が自動的に適切な型を推測してくれます。

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

const strValue = identity("TypeScript");
const numValue = identity(123);

この場合、identity関数を呼び出す際に、TypeScriptが自動的に引数の型に基づいてTの型を推論します。したがって、strValueにはstring型、numValueにはnumber型が割り当てられます。ジェネリクスを使う場合でも、型推論によりコードの明示的な型指定が不要になります。

オーバーロードとジェネリクスを組み合わせた型推論

関数オーバーロードとジェネリクスを組み合わせることで、さらに柔軟で効率的な型推論を実現できます。以下の例では、オーバーロードされたジェネリック関数が異なる型に対して動的に対応しています。

function combine<T>(a: T[], b: T[]): T[];
function combine<T, U>(a: T[], b: U[]): (T | U)[];
function combine(a: any[], b: any[]): any[] {
    return a.concat(b);
}

const combinedArray1 = combine([1, 2, 3], [4, 5]); // number[]
const combinedArray2 = combine([1, 2, 3], ["a", "b"]); // (number | string)[]

この例では、2つの異なる型のオーバーロードが存在し、それに応じて適切な型推論が行われています。combinedArray1number[]型として推論され、combinedArray2(number | string)[]型と推論されます。ジェネリクスとオーバーロードを組み合わせることで、さまざまな型のパターンに対応する関数を効率よく作成できます。

型推論の利点と考慮点

型推論を活用することで、次のような利点が得られます。

  1. コードの簡潔さ: 型を明示する必要がないため、コードがシンプルになり、可読性が向上します。
  2. 開発効率の向上: 型推論によって開発者の負担が軽減され、迅速なコーディングが可能になります。
  3. 型安全性の維持: 型推論が適切な型を自動的に割り当てるため、型安全なコードが自然に書けます。

一方で、型推論に頼りすぎると、複雑なジェネリクスやオーバーロードで意図しない型が推論される可能性もあります。そのため、必要に応じて型を明示することも考慮すべきです。

関数オーバーロードと型推論の連携を活用することで、柔軟かつ型安全なコードが実現でき、TypeScriptの強力な型システムをフルに活用することが可能となります。

ジェネリクスを使ったコードのメンテナンス性向上

ジェネリクスを使うことで、コードのメンテナンス性が大幅に向上します。特に、大規模なプロジェクトや複数の型を扱う場面では、ジェネリクスを活用することでコードの再利用性が高まり、変更に強い設計が可能になります。また、型安全性が強化されることで、将来的なバグの発生リスクを低減し、メンテナンスを容易にする効果もあります。

再利用可能なコードの作成

ジェネリクスを使うと、特定の型に依存しない再利用可能な関数やクラスを作成できます。これにより、同じロジックを異なる型に対して適用する場合でも、ジェネリクスを利用することで同じコードを使い回すことができ、コード量を削減しつつメンテナンスしやすい構造が実現します。

例えば、配列の操作を行う関数をジェネリクスを使って実装すると、型に関わらず再利用できます。

function getLastElement<T>(arr: T[]): T {
    return arr[arr.length - 1];
}

const lastNumber = getLastElement([1, 2, 3]); // 3
const lastString = getLastElement(["a", "b", "c"]); // "c"

このように、getLastElement関数はnumber[]string[]など、どの型の配列でも対応できるため、再利用可能な汎用関数として活用できます。

将来的な拡張に強い設計

ジェネリクスを用いた設計は、将来的に型の追加や変更が発生しても、関数やクラスを再定義する必要がないため、変更に対して柔軟です。新しい型を追加する場合でも、既存のジェネリックなコードがそのまま使用できるため、メンテナンスが容易です。

たとえば、以下の例では、将来的にnumber[]以外の型を扱う必要が生じた場合でも、既存のコードに変更を加えずに対応可能です。

function mergeArrays<T>(arr1: T[], arr2: T[]): T[] {
    return arr1.concat(arr2);
}

const numbers = mergeArrays([1, 2, 3], [4, 5, 6]); // [1, 2, 3, 4, 5, 6]
const strings = mergeArrays(["a", "b"], ["c", "d"]); // ["a", "b", "c", "d"]

ここでは、Tを使うことで、number[]string[]などの異なる型の配列を統合するロジックを再利用できています。将来、他の型(たとえば、boolean[]object[])を扱う際にも、同じ関数を使うことができます。

バグ防止と型安全性の強化

ジェネリクスを使うことで、型に依存するバグを防ぎやすくなります。型を明示的に定義するよりも、ジェネリクスを使って関数やクラスが適切に型を推論しながら動作するようにすることで、異なる型が混在してしまうようなバグを未然に防ぐことができます。

例えば、次のようなコードでは、異なる型の配列が誤って混在することを防ぐことができます。

function concatenate<T>(arr1: T[], arr2: T[]): T[] {
    return arr1.concat(arr2);
}

const mixedArray = concatenate([1, 2, 3], ["a", "b"]); // エラー: 異なる型は結合できない

このように、ジェネリクスを使うことで、型の整合性を保ちながらコードの安全性を確保できるため、バグが発生しにくい構造が作れます。

可読性と理解のしやすさ

ジェネリクスを適切に使うことで、コードの可読性も向上します。型が明確であり、関数やクラスがどのようなデータ型を受け取るかが直感的に理解できるため、チーム内でのコード共有やメンテナンスがしやすくなります。

function wrapInArray<T>(value: T): T[] {
    return [value];
}

const wrappedNumber = wrapInArray(42); // [42]
const wrappedString = wrapInArray("hello"); // ["hello"]

このように、wrapInArray関数は引数の型に関わらず動作し、読みやすいコードとして理解しやすいです。ジェネリクスを使った関数やクラスは、型が一貫しているため、読み手にとってもどのように動作するかが把握しやすく、メンテナンス時にも容易に修正が可能です。

まとめ

ジェネリクスを活用することで、コードの再利用性を高め、変更に強い柔軟な設計が可能になります。また、型安全性が強化されることで、バグの発生リスクを軽減し、将来のメンテナンス性を向上させることができます。

実践演習問題

ジェネリクスを使った関数オーバーロードの理解を深めるために、以下の演習問題に取り組んでみましょう。これらの問題は、ジェネリクスや関数オーバーロードの実践的な応用を体験できるように設計されています。

問題1: 配列のフィルタリング

異なる型の配列をジェネリクスを用いてフィルタリングする関数filterArrayを実装してください。この関数は、渡された配列から特定の値を除外する機能を持ちます。

function filterArray<T>(arr: T[], value: T): T[] {
    // 配列から特定の値を除外するロジックを実装してください
    return arr.filter(item => item !== value);
}

// 以下の例で動作することを確認してください。
const numbers = filterArray([1, 2, 3, 4], 2); // [1, 3, 4]
const strings = filterArray(["apple", "banana", "cherry"], "banana"); // ["apple", "cherry"]

問題2: オブジェクトのプロパティを取得する関数

ジェネリクスを使用して、オブジェクトの特定のプロパティを取得する関数getPropertyを実装してください。この関数は、オブジェクトとプロパティ名を引数に取り、指定されたプロパティの値を返します。

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    // 指定されたプロパティの値を返すロジックを実装してください
    return obj[key];
}

// 以下の例で動作することを確認してください。
const person = { name: "John", age: 30 };
const name = getProperty(person, "name"); // "John"
const age = getProperty(person, "age"); // 30

問題3: 関数オーバーロードを使った複数の型の連結

次に、関数オーバーロードを使って、文字列や数値を結合する関数concatValuesを実装してください。この関数は、2つの文字列または2つの数値を受け取り、それらを結合して返します。

function concatValues(a: string, b: string): string;
function concatValues(a: number, b: number): number;
function concatValues(a: any, b: any): any {
    // 2つの値を結合するロジックを実装してください
    return a + b;
}

// 以下の例で動作することを確認してください。
const result1 = concatValues("Hello, ", "World!"); // "Hello, World!"
const result2 = concatValues(10, 20); // 30

問題4: 配列の要素をペアにする関数

ジェネリクスを使って、配列の要素を2つずつペアにして返す関数pairElementsを作成してください。この関数は、配列の要素を2つの要素を持つタプルに分割し、タプルの配列を返します。

function pairElements<T>(arr: T[]): [T, T][] {
    // 配列の要素をペアにして返すロジックを実装してください
    const result: [T, T][] = [];
    for (let i = 0; i < arr.length; i += 2) {
        result.push([arr[i], arr[i + 1]]);
    }
    return result;
}

// 以下の例で動作することを確認してください。
const pairedNumbers = pairElements([1, 2, 3, 4]); // [[1, 2], [3, 4]]
const pairedStrings = pairElements(["a", "b", "c", "d"]); // [["a", "b"], ["c", "d"]]

まとめ

これらの演習問題を通じて、ジェネリクスや関数オーバーロードの使い方をより実践的に理解できるようになるはずです。実装しながら、どのようにジェネリクスが型安全性と再利用性を向上させるかを体験してください。

まとめ

本記事では、TypeScriptにおけるジェネリクスと関数オーバーロードの基本概念から、実装方法や応用例、メンテナンス性の向上までを解説しました。ジェネリクスとオーバーロードを組み合わせることで、柔軟かつ型安全な関数を作成でき、再利用性が高まり、コードの拡張性も向上します。また、実践演習問題を通じて、実際にジェネリクスを活用する際の具体的なシナリオを体験できたかと思います。これらの知識を活かして、より効率的でメンテナブルなコードを書けるようになるでしょう。

コメント

コメントする

目次