🌱

TypeScript入門②

に公開

前回の記事では、プリミティブ型、配列、タプル型、オブジェクト型、関数、クラス、非同期処理など基本的な型から応用的な機能までを網羅しました。

今回は TypeScript のさらに強力な機能である「ジェネリクス(Generics)」について深掘りしていきます。ジェネリクスを使いこなすことで、TypeScript コードはより柔軟に、そして安全になります。

なぜジェネリクスが必要なのか?

異なる型なのに「同じような処理」をしたい場面があります。
ジェネリクスを使用しない場合、以下の問題が発生します。

  • any 型の使用: any 型を使うと、どんな型でも受け入れられますが、TypeScript の型チェックの恩恵を失い、せっかくの型安全性が損なわれてしまいます。
  • 型の重複: 特定の型ごとに同じような関数を複数定義すると、コードが冗長になり、保守が大変になります。
// any を使うと型情報が失われる
function identityAny(arg: any): any {
  return arg;
}
const resultAny = identityAny("hello");

// 型ごとに似たような関数を定義する必要がある
function identityString(arg: string): string {
  return arg;
}
function identityNumber(arg: number): number {
  return arg;
}
// ...他の型も必要になるたびに増えていく

このような問題を解決し、型安全性を保ちながらコードの再利用性を高めるために、ジェネリクスは非常に重要な役割を果たします。

ジェネリクスとは

「型引数(Type Parameters)」を導入することで、様々なデータ型に対応できる汎用的な(ジェネリックな)コンポーネントを作成する機能です。
< > を使って型引数を定義します。

// 💡 ジェネリクスを使った関数の例
// <T> が型引数です。T はこの関数が受け取る引数 `arg` の型であり、
// 同時に戻り値の型も T であることを示します。
function identity<T>(arg: T): T {
  return arg;
}

// 💡 関数を呼び出すときに具体的な型を指定
// string 型の値を渡したので、T は string になる
let outputString = identity<string>("myString");

// number 型の値を渡したので、T は number になる
let outputNumber = identity<number>(123);

ジェネリクスを使うメリット

再利用性の向上(汎用的なコードの作成)

特定のデータ型に縛られない、より汎用的なコンポーネントや関数を作成できます。

型安全性の維持(anyからの脱却)

any型を使わずに型情報を保持したまま汎用的なコードを書けます。
実行時エラーの早期発見 / 正確なコード補完が提供され、開発効率が上がります。

コードの簡潔性

オーバーロード(同じ名前で引数の型が異なる関数を複数定義すること)のような冗長な記述を避け、よりクリーンで簡潔なコードを書けるようになります。

制約 (Constraints)

ジェネリクスは非常に汎用的なコードを書ける反面、「どんな型でも受け入れる」ために、その型が持っているはずのプロパティやメソッドにアクセスできないという問題に直面することがあります。

// 💡 エラーになる例: T が length プロパティを持っている保証がない
function printLength<T>(arg: T): void {
  console.log(arg.length); // ❌ エラー:Property 'length' does not exist on type 'T'.
}

printLength(10); // number型にはlengthがない

このような場合に制約を使用し、ジェネリック型が満たすべき条件(特定のプロパティを持っていること、特定のインターフェースを実装していることなど)を指定します。

制約は、型引数の後ろに extends キーワードを使って記述します。

// 💡 T は length プロパティを持つ型でなければならない、という制約です
interface HasLength {
  length: number;
}

// 💡 <T extends HasLength> と記述することで、T は HasLength インターフェースを実装する型に限定されます
function printLength<T extends HasLength>(arg: T): void {
  console.log(`長さ: ${arg.length}`); // ✅ OK: T は length を持つことが保証されています
}

// ✅ OK: string は length プロパティを持ちます
printLength("Hello TypeScript"); // 長さ: 16

// ✅ OK: 配列も length プロパティを持ちます
printLength([1, 2, 3, 4, 5]); // 長さ: 5

// ❌ エラー:number型は length プロパティを持たないので制約を満たしません
printLength(10); // ❌ エラー:Argument of type 'number' is not assignable to parameter of type 'HasLength'.

ジェネリクスの基本的な使い方

ジェネリクスは、関数だけでなく、インターフェースやクラス、型エイリアスなど、様々な場所で利用できます。

関数でのジェネリクス

最も一般的なジェネリクスの使い方の一つが関数です。
引数として受け取る型と、戻り値の型を柔軟に定義できるため、多様なデータ型に対応する汎用的な関数を作成できます。

// 💡 どんな型のデータも受け取って、配列として返す関数
function wrapInArray<T>(value: T): T[] {
  return [value];
}

let strings = wrapInArray("hello"); // strings: string[] と推論されます
let numbers = wrapInArray(123); // numbers: number[] と推論されます

インターフェースや型エイリアスでのジェネリクス

インターフェースや型エイリアスでもジェネリクスを使用することで、様々な型に対応できる汎用的なデータ構造を定義できます。
APIのレスポンスや共通のデータコンテナを定義する際に特に便利です。

// 💡 任意の型の値を格納できるコンテナインターフェース
interface Box<T> {
  value: T;
}

let stringBox: Box<string> = { value: "TypeScript" };
let numberBox: Box<number> = { value: 123 };

// 💡 API レスポンスなど、様々なデータ型に対応する型エイリアス
type ApiResponse<Data> = {
  status: "success" | "error";
  data?: Data;
  message?: string;
};

type User = { id: number; name: string };
let userResponse: ApiResponse<User> = {
  status: "success",
  data: { id: 1, name: "Alice" },
};

クラスでのジェネリクス

クラスでもジェネリクスを使うことで、特定の型に依存しない、汎用的なクラスを作成できます例えば、任意の型の要素を扱うスタックやキューのようなデータ構造を実装する際に役立ちます。

// 💡 任意の型のアイテムを格納できるシンプルなスタッククラス
class Stack<T> {
  private elements: T[] = [];

  push(element: T): void {
    this.elements.push(element);
  }

  pop(): T | undefined {
    return this.elements.pop();
  }

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

let numberStack = new Stack<number>();
numberStack.push(10);

参考リンク

https://typescriptbook.jp/

Discussion