TypeScriptでクラス内ジェネリクスの使い方と型推論の徹底解説

TypeScriptは、JavaScriptに型システムを追加することで、より安全で効率的なコードを書けるようにするための言語です。その中でも、ジェネリクス(Generics)は非常に強力な機能で、クラスや関数に柔軟な型を持たせることが可能です。ジェネリクスを使うことで、さまざまなデータ型に対応するコードを1つのクラスや関数で記述でき、コードの再利用性や保守性を高めることができます。

本記事では、TypeScriptのクラスにおけるジェネリクスの使い方と、TypeScriptの特徴である型推論の仕組みを詳しく解説します。ジェネリクスを正しく活用することで、型の安全性を保ちながら、柔軟で効率的なコードが書けるようになります。

目次

TypeScriptにおけるジェネリクスとは

ジェネリクスとは、クラスや関数、インターフェースに対して汎用的な型を指定できる機能です。ジェネリクスを使うことで、型を固定せずに、異なる型を持つデータに対応する柔軟なコードを記述できます。これにより、異なる型を受け入れながらも、型安全性を確保できるのが大きな利点です。

たとえば、ジェネリクスを使わない場合、複数の型に対応するために同じ機能を持つ異なるクラスや関数を作成しなければならない場合があります。しかし、ジェネリクスを使うことで、1つのクラスや関数でさまざまな型に対応でき、コードの重複を防ぐことができます。

ジェネリクスを用いると、以下のように型の汎用性を高めたコードを書くことができます。

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

この関数は、引数として渡される型に応じて自動的に型が決まり、どのような型でも対応可能です。このように、ジェネリクスは型の柔軟性を高めながらも、TypeScriptの型安全性を最大限に活かすことができる便利な機能です。

クラス内でのジェネリクスの活用方法

TypeScriptのクラス内でもジェネリクスを利用することが可能で、これにより柔軟で汎用的なクラス設計が可能になります。ジェネリクスをクラスに適用すると、インスタンスを生成する際に具体的な型を指定できるようになります。この仕組みは、異なる型のデータを扱う場合でも、クラスを再利用できるため、効率的かつ安全にコードを記述できます。

ジェネリクスを使ったクラスの定義は、次のように記述します。

class DataStorage<T> {
  private data: T[] = [];

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

  removeItem(item: T) {
    this.data = this.data.filter(i => i !== item);
  }

  getItems(): T[] {
    return [...this.data];
  }
}

このクラス DataStorage は、ジェネリクス <T> を使用して型をパラメータ化しています。これにより、T が何であるかを指定しない限り、このクラスはどの型のデータでも扱えるようになります。

たとえば、文字列や数値を扱う場合、以下のように使用できます。

const textStorage = new DataStorage<string>();
textStorage.addItem('Hello');
textStorage.addItem('World');

const numberStorage = new DataStorage<number>();
numberStorage.addItem(42);
numberStorage.addItem(100);

このように、ジェネリクスを使うことで、型に依存しない汎用的なクラスを簡単に作成することができます。型を明確にすることで、型エラーの発生を防ぎ、コードの可読性と安全性を高めることができます。

コンストラクタでのジェネリクスの利用

TypeScriptのクラスにおいて、コンストラクタでもジェネリクスを使用することができます。コンストラクタ内でジェネリクスを用いると、クラスのインスタンス生成時に型を指定し、その型に基づいたデータの初期化や処理を行うことができます。これにより、より柔軟なクラス設計が可能になります。

次の例では、コンストラクタでジェネリクスを使って型を指定し、その型に基づいてデータを初期化するクラスを示しています。

class KeyValuePair<K, V> {
  private key: K;
  private value: V;

  constructor(key: K, value: V) {
    this.key = key;
    this.value = value;
  }

  getKey(): K {
    return this.key;
  }

  getValue(): V {
    return this.value;
  }
}

このクラス KeyValuePair は、キーと値を保持するために2つのジェネリクス KV を使用しています。コンストラクタは、インスタンスを生成する際に KV の具体的な型を受け取り、その型に基づいてプロパティを初期化します。

インスタンスを生成するときに、次のように型を指定します。

const pair1 = new KeyValuePair<string, number>('age', 30);
console.log(pair1.getKey()); // 'age'
console.log(pair1.getValue()); // 30

const pair2 = new KeyValuePair<boolean, string>(true, 'Success');
console.log(pair2.getKey()); // true
console.log(pair2.getValue()); // 'Success'

この例では、KeyValuePair クラスはキーとして文字列やブール型、値として数値や文字列を保持することができます。このように、コンストラクタでジェネリクスを使うと、型安全で柔軟なオブジェクト生成が可能になります。

メソッド内でのジェネリクスの活用例

クラスのメソッド内でもジェネリクスを活用することができます。メソッドにジェネリクスを導入することで、異なる型の引数や返り値に対応する汎用的なメソッドを定義することが可能です。これにより、コードの再利用性を高めつつ、型安全性も維持できます。

次の例では、ジェネリクスを使ったメソッドを持つクラスを示します。

class Utility {
  // メソッドにジェネリクスを適用
  static merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
  }

  // ジェネリクスで配列からランダムな要素を取得
  static getRandomElement<T>(items: T[]): T {
    const randomIndex = Math.floor(Math.random() * items.length);
    return items[randomIndex];
  }
}

この Utility クラスには、2つのジェネリクスを使用したメソッドがあります。

  1. merge メソッド: 2つの異なるオブジェクトを受け取り、それらをマージして新しいオブジェクトを返します。このメソッドは、異なる型を持つオブジェクトを扱うため、ジェネリクス TU を用いて型安全にマージを行います。
  2. getRandomElement メソッド: 配列の中からランダムな要素を取得します。このメソッドもジェネリクス T を使用して、どの型の配列にも対応可能にしています。

このクラスのメソッドを利用する例を見てみましょう。

const mergedObject = Utility.merge({ name: 'Alice' }, { age: 30 });
console.log(mergedObject); // { name: 'Alice', age: 30 }

const numbers = [1, 2, 3, 4, 5];
const randomNum = Utility.getRandomElement(numbers);
console.log(randomNum); // 1 〜 5 のいずれか

この例では、merge メソッドで異なる型のオブジェクトを安全にマージし、getRandomElement メソッドで配列内のランダムな要素を取得しています。

このように、メソッド内でジェネリクスを活用すると、型の柔軟性を持ちながら、型推論によって型安全な操作が可能になります。特に再利用性の高いユーティリティ関数を作成する際に、ジェネリクスは非常に有効です。

型推論とジェネリクスの相互関係

TypeScriptにおける型推論とジェネリクスは密接に連携して動作します。ジェネリクスを使用することで、より柔軟なコードが書けるだけでなく、TypeScriptの強力な型推論機能により、明示的に型を指定しなくても、コンパイラが適切な型を自動的に推論してくれます。

通常、ジェネリクスを使用する場合は型パラメータを指定しますが、多くの場面でTypeScriptはその型を推論してくれるため、コードがさらに簡潔になります。次の例を見てみましょう。

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

// 明示的に型を指定
const result1 = identity<number>(10);

// 型推論による自動推定
const result2 = identity('Hello');

上記の identity 関数はジェネリクス T を使っており、引数 arg の型が自動的に推論されます。result1 の場合は、型 number を明示的に指定していますが、result2 の場合は引数に渡された値 'Hello' から T が自動的に string だと推論されます。これにより、開発者が手動で型を指定する手間を省きつつ、型の安全性が確保されます。

さらに、クラスやメソッドでも型推論とジェネリクスが連携して動作します。以下の例を見てください。

class Box<T> {
  content: T;

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

  getContent(): T {
    return this.content;
  }
}

// インスタンス生成時に型推論が自動的に行われる
const stringBox = new Box('TypeScript');
console.log(stringBox.getContent()); // 'TypeScript'

const numberBox = new Box(100);
console.log(numberBox.getContent()); // 100

この Box クラスは、ジェネリクス T を使って型をパラメータ化していますが、インスタンスを生成する際に渡された値に基づいて T の型が自動的に推論されます。例えば、stringBox では Tstring に、numberBox では Tnumber に推論されています。

型推論とジェネリクスの相互作用によって、コードの可読性と安全性が高まり、開発者は複雑な型定義を意識することなく、効率的にコードを記述することができます。TypeScriptはこの推論能力を活かして、開発者にとって手間を減らしながらも、型の一貫性を保証します。

制約を持たせたジェネリクスの使い方

TypeScriptでは、ジェネリクスに「制約」を持たせることができます。これにより、ジェネリクスに適用できる型に対して、特定の条件を課すことができ、より型安全で特定の型の特性を活かした設計が可能になります。制約を持たせることによって、ジェネリクスで利用できる型に特定のプロパティやメソッドが存在することを保証でき、開発者はその型の特定の機能を安心して利用できます。

次の例では、オブジェクト型に制約を設け、プロパティの存在を保証する方法を示します。

interface HasLength {
  length: number;
}

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

この logLength 関数では、ジェネリクス T に対して HasLength というインターフェースを制約として課しています。HasLength インターフェースには length プロパティが定義されており、T にこのインターフェースを継承させることで、arg.length が安全に使用できるようになります。これにより、length プロパティを持つ型に対してのみ、この関数が使用可能になります。

次に、logLength 関数を使った例を見てみましょう。

logLength("Hello"); // OK, 文字列には length プロパティがある
logLength([1, 2, 3]); // OK, 配列にも length プロパティがある

// logLength(123); // エラー, 数値には length プロパティがない

ここでは、文字列や配列のように length プロパティを持つデータ型に対しては関数が正常に動作しますが、number 型のように length プロパティを持たない型ではコンパイル時にエラーが発生します。このように、制約を設けることで、特定の型やプロパティに依存したロジックを型安全に実装できます。

また、オブジェクトの特定のキーにアクセスする関数を制約付きジェネリクスで作成する例も示します。

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const person = { name: "John", age: 30 };
const name = getProperty(person, "name"); // OK
const age = getProperty(person, "age");   // OK

// getProperty(person, "height"); // エラー, 'height' プロパティは存在しない

この場合、KT 型のキーに制約されています。これにより、指定された key がオブジェクト person のプロパティであることを保証し、存在しないキーを指定した場合にはコンパイルエラーが発生します。

制約を持たせたジェネリクスは、柔軟でありながら型安全性を高める重要な手法です。これにより、特定の条件を満たす型に対してのみ動作する関数やクラスを設計することができ、コードの信頼性が大幅に向上します。

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

TypeScriptのジェネリクスは、実際のプロジェクトでもさまざまな場面で活用されています。特に、汎用的なデータ構造やAPI設計において、ジェネリクスを使うことで柔軟性と型安全性を同時に実現できます。ここでは、いくつかの実際のプロジェクトでのジェネリクスの応用例を紹介します。

1. リスト管理クラスの作成

例えば、複数の型に対応するリスト管理クラスをジェネリクスを用いて実装することができます。このクラスは、異なるデータ型を格納できる汎用的なリストを管理し、データの追加、削除、取得といった操作を行います。

class ListManager<T> {
  private items: T[] = [];

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

  removeItem(item: T): void {
    this.items = this.items.filter(i => i !== item);
  }

  getItems(): T[] {
    return this.items;
  }
}

この ListManager クラスは、ジェネリクス T を用いることで、どの型のリストにも対応できる汎用的なクラスとなります。実際のプロジェクトでは、異なる型のデータを一貫した方法で管理することが多いため、ジェネリクスを用いることでコードの再利用性と柔軟性を高められます。

const stringList = new ListManager<string>();
stringList.addItem("TypeScript");
stringList.addItem("Generics");

const numberList = new ListManager<number>();
numberList.addItem(10);
numberList.addItem(20);

このように、ListManager クラスは、文字列や数値など、さまざまな型に対応したリストを管理できるため、プロジェクト内で使い回しやすい設計となっています。

2. フォーム入力のバリデーション

実際のWebアプリケーションでは、フォーム入力のバリデーションが重要です。ここでもジェネリクスを使うことで、異なるデータ型に対して共通のバリデーションロジックを適用することができます。

interface Validator<T> {
  isValid(value: T): boolean;
}

class StringValidator implements Validator<string> {
  isValid(value: string): boolean {
    return value.length > 0;
  }
}

class NumberValidator implements Validator<number> {
  isValid(value: number): boolean {
    return value >= 0;
  }
}

ここでは、Validator インターフェースにジェネリクス T を適用し、文字列や数値など異なる型に対応したバリデーションクラスを定義しています。この方法により、型ごとのバリデーションロジックを簡単に拡張することができ、フォームの入力チェックを効率化できます。

3. 型安全なAPIリクエストの設計

さらに、APIリクエストやレスポンスの型を安全に扱うために、ジェネリクスを使った設計は非常に役立ちます。例えば、APIリクエストの結果に基づいて型を指定することで、データ処理時に型エラーを防ぐことができます。

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

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return { status: response.status, data };
}

この fetchData 関数は、ジェネリクス T を用いてAPIのレスポンスデータの型を指定することができます。これにより、どのようなデータ型のレスポンスが返ってくるかを明確にし、型安全にデータを扱えるようになります。

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

const userData = await fetchData<User>('/api/user/1');
console.log(userData.data.name);  // 型安全に `name` プロパティにアクセス

この例では、APIからユーザー情報を取得し、レスポンスの型を User として指定することで、型に基づいた安全なデータ操作が可能になります。API設計時にジェネリクスを導入することで、型推論によってデータ操作のミスを防ぐことができ、開発の生産性を向上させることができます。


このように、ジェネリクスを使うことで、実際のプロジェクトにおいても汎用的で柔軟なコードを記述でき、型安全性と再利用性を兼ね備えた設計が可能になります。実際の開発では、複雑なデータ構造やAPI、バリデーションといったさまざまな場面でジェネリクスが役立ちます。

ジェネリクスを使った型安全なAPI設計

ジェネリクスは、型安全なAPI設計において非常に重要な役割を果たします。APIが返すデータの型が不明瞭であったり、エラーが発生したりすると、開発者は手動で型をキャストする必要が生じ、コードの安全性が損なわれる可能性があります。ジェネリクスを使うことで、APIリクエストの入力やレスポンスのデータ型を明確に指定し、型安全性を保ちながら柔軟な設計が可能になります。

1. APIリクエストの型安全な実装

ジェネリクスを使って型安全なAPIリクエストを設計する方法の一例として、次のような汎用的なAPI呼び出し関数を作成できます。

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

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return { status: response.status, data };
}

この fetchData 関数は、ジェネリクス T を使って、APIから取得されるデータ型を引数として指定しています。これにより、APIのレスポンスの型を実行時に明確にし、取得するデータの型安全性を確保できます。

2. 使用例: ユーザーデータの取得

次に、fetchData 関数を使って、具体的なAPIエンドポイントからユーザーデータを取得する例を見てみましょう。

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

const userResponse = await fetchData<User>('/api/user/123');
console.log(userResponse.data.name); // 型推論により安全にアクセス可能

この例では、User インターフェースを定義し、APIから取得するデータの型を指定しています。これにより、レスポンスのデータ型が User であることが保証され、IDEやコンパイラが型チェックを行うため、型の間違いやプロパティの誤アクセスを防ぐことができます。

3. APIエラーハンドリングとジェネリクス

API呼び出しにおいて、レスポンスが成功する場合だけでなく、エラーが発生する場合も考慮する必要があります。ジェネリクスを使えば、成功時とエラー時のデータ型を分けて安全に処理できます。

interface ErrorResponse {
  message: string;
  errorCode: number;
}

async function fetchDataWithError<T>(
  url: string
): Promise<ApiResponse<T> | ErrorResponse> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error('Failed to fetch');
    }
    const data = await response.json();
    return { status: response.status, data };
  } catch (error) {
    return { message: error.message, errorCode: 500 };
  }
}

この fetchDataWithError 関数では、ジェネリクス T によって、成功した場合には ApiResponse<T> 型のデータを、エラーが発生した場合には ErrorResponse 型のデータを返すようにしています。このようにジェネリクスを使うことで、APIの成功時と失敗時のレスポンスを型で明示的に区別することができ、安全なエラーハンドリングが可能になります。

4. 複雑なAPIレスポンスの扱い

現実のプロジェクトでは、APIのレスポンスが単純なデータ型ではなく、複数の型を含む複雑なオブジェクトであることが一般的です。ジェネリクスを活用すれば、このような複雑なレスポンス型も型安全に扱えます。

interface PaginatedResponse<T> {
  total: number;
  items: T[];
}

async function fetchPaginatedData<T>(
  url: string
): Promise<ApiResponse<PaginatedResponse<T>>> {
  const response = await fetch(url);
  const data = await response.json();
  return { status: response.status, data };
}

この fetchPaginatedData 関数では、ページネーション(複数ページに分割されたデータ)を扱うレスポンスに対応しています。ジェネリクス T により、ページ内の各アイテムの型を指定することができ、例えばユーザーデータや商品データなど、異なる型のリストを柔軟に処理できます。

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

const productResponse = await fetchPaginatedData<Product>('/api/products');
console.log(productResponse.data.items[0].name); // 型安全にアクセス可能

このように、ジェネリクスを用いてAPIのレスポンスを型安全に扱うことで、複雑なデータ構造でもエラーを防ぎ、正確なデータ操作が可能になります。


ジェネリクスを使った型安全なAPI設計は、プロジェクト全体の信頼性を高め、予期せぬエラーを防ぐ上で非常に有効です。型の安全性を保ちながら柔軟なAPIを構築できるため、開発者は自信を持ってデータを操作することができます。

TypeScriptにおけるジェネリクスのベストプラクティス

ジェネリクスはTypeScriptで強力な機能ですが、適切に使うことでコードの可読性と保守性を高め、複雑なシステムでもエラーのない安全なコードを書くことが可能です。ここでは、TypeScriptのジェネリクスを使ったベストプラクティスをいくつか紹介します。

1. 明確でわかりやすいジェネリック型パラメータ名を使う

ジェネリック型のパラメータ名は、できるだけ意味がわかりやすいものを使用することが重要です。一般的に、シンプルな TU などの短い名前が使われることが多いですが、複雑なコードになると、より具体的な名前を使用することで可読性が向上します。

悪い例:

function getItems<T>(items: T[]): T[] {
  return items;
}

良い例:

function getItems<ItemType>(items: ItemType[]): ItemType[] {
  return items;
}

パラメータ名を具体的にすることで、型の意図が明確になり、コードの可読性が高まります。特に、大規模なコードベースや複数のジェネリクスを使う場合は、わかりやすい命名が役立ちます。

2. 必要なときだけジェネリクスを使用する

ジェネリクスは強力なツールですが、すべての場面で使う必要はありません。型推論がうまく働く場合には、明示的にジェネリクスを使用せず、TypeScriptの型推論に任せることがコードのシンプル化につながります。

悪い例:

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

const result = identity<string>('Hello');

良い例:

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

const result = identity('Hello'); // 型推論が働く

TypeScriptの型推論を活かすことで、コードが簡潔になり、読みやすくなります。ジェネリクスを無理に指定する必要がない場合は、型推論を使うことを検討しましょう。

3. 制約を使って型安全性を高める

ジェネリクスは非常に柔軟ですが、必要に応じて型に制約を設けることで、特定の型のみに適用可能なロジックを記述できます。これにより、より強力な型安全性を実現し、予期せぬ型エラーを防止します。

悪い例:

function printLength<T>(value: T): void {
  console.log(value.length); // エラーが発生する可能性がある
}

良い例:

interface HasLength {
  length: number;
}

function printLength<T extends HasLength>(value: T): void {
  console.log(value.length);
}

制約を追加することで、ジェネリクスを使った関数やクラスが適切な型に対してのみ動作することを保証し、エラーのリスクを減らします。

4. 再利用可能なユーティリティ関数を作成する

ジェネリクスを使うと、再利用可能なユーティリティ関数を作成し、異なる型に対して同じ処理を適用することができます。このような関数はプロジェクト全体で使い回せるため、コードの冗長性を減らし、メンテナンスがしやすくなります。

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

const merged = mergeObjects({ name: 'Alice' }, { age: 30 });
console.log(merged); // { name: 'Alice', age: 30 }

このようなジェネリクスを活用したユーティリティ関数を設計することで、さまざまな場面で再利用可能なコードを効率的に書くことができます。

5. ジェネリクスのデフォルト型を活用する

ジェネリクスにはデフォルトの型を設定することが可能です。これにより、呼び出し側で型を省略した場合に、予期しない型エラーを回避しつつ、型安全な動作を保証できます。

function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

const stringArray = createArray(3, 'TypeScript'); // string[]型
const numberArray = createArray<number>(3, 100);  // number[]型

デフォルト型を設定することで、ジェネリクスを使った関数やクラスをさらに柔軟にし、使用者にとって使いやすいインターフェースを提供できます。


これらのベストプラクティスを守ることで、TypeScriptのジェネリクスを効果的に活用し、型安全かつ柔軟なコードを作成することができます。ジェネリクスは強力ですが、適切な使い方をすることで、コードの品質と保守性が大幅に向上します。

演習問題:ジェネリクスを使ったクラスの実装

ここでは、ジェネリクスを使って、クラスを実装する演習問題に挑戦してみましょう。以下の指示に従って、ジェネリクスを使用した型安全なクラスを作成してください。

演習1: スタック(Stack)クラスの実装

ジェネリクスを使って、汎用的なスタック(LIFO: Last In First Out)データ構造を実装してください。このスタックは、任意の型の要素を追加、削除、取得できる機能を持っています。

スタッククラスの要件

  1. ジェネリクスを使用して、任意の型のデータを格納できるようにします。
  2. 次のメソッドを実装します:
  • push(item: T) – スタックに新しいアイテムを追加します。
  • pop(): T | undefined – スタックの最上部のアイテムを取り出して返します。スタックが空の場合は undefined を返します。
  • peek(): T | undefined – スタックの最上部のアイテムを確認しますが、取り出さずにそのままにします。スタックが空の場合は undefined を返します。
  • isEmpty(): boolean – スタックが空かどうかを確認します。
  • size(): number – スタックのサイズを返します。

サンプルコード

次のヒントをもとにクラスを実装してください。

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];
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }

  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

const stringStack = new Stack<string>();
stringStack.push('TypeScript');
stringStack.push('Generics');
console.log(stringStack.pop()); // 'Generics'

演習2: 制約付きジェネリクスを使ったクラス

次に、制約付きジェネリクスを使ってクラスを拡張します。以下の要件に基づいて、新しいクラスを作成してください。

要件

  1. キーと値のペアを管理するクラスを作成します。このクラスは、ジェネリクスを使って任意のキーと値を扱います。
  2. K 型のキーに対して toString() メソッドが利用可能であることを制約として設定してください。
  3. 次のメソッドを実装します:
  • set(key: K, value: V): void – 新しいキーと値のペアを追加します。
  • get(key: K): V | undefined – キーに対応する値を取得します。
  • delete(key: K): void – キーを削除します。
  • keys(): K[] – すべてのキーを返します。

サンプルコード

制約付きジェネリクスを使ったクラスの実装例です。

class KeyValuePair<K extends string | number, V> {
  private map: Record<K, V> = {} as Record<K, V>;

  set(key: K, value: V): void {
    this.map[key] = value;
  }

  get(key: K): V | undefined {
    return this.map[key];
  }

  delete(key: K): void {
    delete this.map[key];
  }

  keys(): K[] {
    return Object.keys(this.map) as K[];
  }
}

使用例

const store = new KeyValuePair<string, number>();
store.set("apple", 10);
store.set("banana", 20);
console.log(store.get("apple")); // 10
store.delete("banana");
console.log(store.keys()); // ['apple']

この演習を通して、ジェネリクスの基本的な使い方だけでなく、制約を活用した高度な設計も学ぶことができました。ジェネリクスを使うことで、さまざまな型に対応する再利用可能なコードを簡潔に記述できるようになります。

まとめ

本記事では、TypeScriptにおけるジェネリクスの使い方と型推論の連携、制約付きジェネリクスの活用方法について詳しく解説しました。ジェネリクスを使うことで、型安全で柔軟なクラスや関数を設計でき、再利用性が高く保守性の良いコードを書くことが可能になります。さらに、制約を用いることで、型の安全性をより強化し、現実的なプロジェクトでも効率的に活用できることを学びました。

コメント

コメントする

目次