TypeScript Genericsの使い方完全ガイド|初心者向け解説とエラー解決

TypeScript

TypeScript Genericsの使い方完全ガイド|初心者向け解説とエラー解決

TypeScriptを使用していると、複数の異なる型に対応する必要があるコンポーネントや関数を書く場面が出てきます。このような場合に活躍するのが「ジェネリクス(Generics)」です。本記事では、TypeScript初心者向けにGenericsの基本的な使い方から、よくあるエラーと解決方法までを詳しく解説します。

Genericsとは何か?

Genericsは、型を変数のように扱う機能です。関数やクラス、インターフェースを定義する際に、具体的な型を後で決定することができます。これにより、コードの再利用性を高めながら、型安全性を保つことができます。

Genericsが必要な原因

Genericsが必要になる主な理由は以下の通りです。

1. 型の再利用性の問題

同じ処理をする関数でも、異なる型に対応させようとすると、型定義を何度も書き直す必要が出てきます。

// Genericsを使わない場合
function getStringLength(value: string): number {
  return value.length;
}

function getNumberAsString(value: number): string {
  return value.toString();
}

function getArrayLength(value: string[]): number {
  return value.length;
}

// 型ごとに関数を書き直す必要がある

2. anyの使用による型安全性の喪失

複数の型に対応させようとして、安易にany型を使うと、コンパイル時に型チェックが機能しなくなります。

// anyを使った場合(非推奨)
function getValue(value: any): any {
  return value;
}

const result = getValue(\"hello\");
// resultの型がanyになってしまい、型安全性が失われる

Genericsの基本的な使い方

関数でのGenerics

関数でGenericsを使う場合、型パラメータを<T>のようにアングルブラケットで囲んで表記します。

// 基本的なGenerics関数
function getValue<T>(value: T): T {
  return value;
}

// 使用例
const stringValue = getValue<string>(\"hello\");
const numberValue = getValue<number>(42);
const boolValue = getValue<boolean>(true);

// 型推論により、型パラメータを省略することも可能
const inferredValue = getValue(\"world\"); // T は自動的に string と推論される

複数の型パラメータ

複数の異なる型を使う場合は、複数の型パラメータを定義できます。

// 複数の型パラメータを使用
function combine<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

// 使用例
const result = combine<string, number>(\"age\", 25);
console.log(result); // [\"age\", 25]

クラスでのGenerics

クラスを定義する際にもGenericsを使うことができます。

// Genericsを使ったクラス定義
class Container<T> {
  private value: T;

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

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

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

// 使用例
const stringContainer = new Container<string>(\"hello\");
console.log(stringContainer.getValue()); // \"hello\"

const numberContainer = new Container<number>(100);
console.log(numberContainer.getValue()); // 100

インターフェースでのGenerics

インターフェースにGenericsを使うことも可能です。

// Genericsを使ったインターフェース定義
interface Repository<T> {
  getById(id: number): T;
  getAll(): T[];
  save(item: T): void;
}

// インターフェースの実装
class UserRepository implements Repository<User> {
  private users: User[] = [];

  getById(id: number): User {
    return this.users.find(u => u.id === id) || new User();
  }

  getAll(): User[] {
    return this.users;
  }

  save(item: User): void {
    this.users.push(item);
  }
}

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

Genericsの制約(Constraints)

型パラメータに制約を加えることで、より安全な型定義ができます。

extendsを使った制約

// T は string | number のいずれかに制限される
function getLength<T extends string | number>(value: T): number {
  if (typeof value === 'string') {
    return value.length;
  }
  return value.toString().length;
}

getLength(\"hello\"); // OK
getLength(123); // OK
// getLength(true); // エラー: boolean は string | number に割り当てられない

キーの制約

// T は K というキーを持つオブジェクトに制限される
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: \"John\", age: 30 };
const name = getProperty(user, \"name\"); // OK、型は string
const age = getProperty(user, \"age\"); // OK、型は number
// const invalid = getProperty(user, \"email\"); // エラー: \"email\" は user のキーではない

オブジェクトの制約

// T はオブジェクト型に制限される
interface HasLength {
  length: number;
}

function getLength<T extends HasLength>(item: T): number {
  return item.length;
}

getLength(\"hello\"); // OK
getLength([1, 2, 3]); // OK
getLength({ length: 5, value: \"test\" }); // OK
// getLength(123); // エラー: number は HasLength を実装していない

実践的なGenericsの例

API レスポンスの型定義

// APIレスポンスのジェネリック型を定義
interface ApiResponse<T> {
  status: number;
  message: string;
  data: T;
}

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

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

// 同じApiResponse型を異なるデータ型で使用
async function fetchUser(userId: number): Promise<ApiResponse<User>> {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

async function fetchProduct(productId: number): Promise<ApiResponse<Product>> {
  const response = await fetch(`/api/products/${productId}`);
  return response.json();
}

// 使用例
const userResponse = await fetchUser(1);
console.log(userResponse.data.name); // User型が推論される

const productResponse = await fetchProduct(1);
console.log(productResponse.data.price); // Product型が推論される

配列操作のジェネリック関数

// 配列の要素をフィルタリングしてマッピングする関数
function filterAndMap<T, U>(
  items: T[],
  predicate: (item: T) => boolean,
  mapper: (item: T) => U
): U[] {
  return items.filter(predicate).map(mapper);
}

// 使用例
const numbers = [1, 2, 3, 4, 5];
const result = filterAndMap(
  numbers,
  (n) => n > 2,
  (n) => n * 2
);
console.log(result); // [6, 8, 10]

const users: User[] = [
  { id: 1, name: \"Alice\", email: \"alice@example.com\" },
  { id: 2, name: \"Bob\", email: \"bob@example.com\" }
];

const names = filterAndMap(
  users,
  (user) => user.name.startsWith(\"A\"),
  (user) => user.name
);
console.log(names); // [\"Alice\"]

よくあるエラーと解決方法

エラー1: 型パラメータの不一致

エラー例:

function getValue<T extends string>(value: T): T {
  return value;
}

// エラー: Argument of type 'number' is not assignable to parameter of type 'string'
getValue(123);

解決方法: 型パラメータの制約を正しく設定し、呼び出す際に適切な型を渡します。

function getValue<T extends string | number>(value: T): T {
  return value;
}

getValue(123); // OK
getValue(\"hello\"); // OK

エラー2: 存在しないプロパティへのアクセス

エラー例:

function getProperty<T>(obj: T, key: string): any {
  return obj[key]; // エラー: Index signature is missing
}

解決方法: keyofを使って、T が持つキーのみを許可します。

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

エラー3: デフォルト型パラメータの未設定

エラー例:

class Container<T> {
  value: T;
  constructor(value: T) {
    this.value = value;
  }
}

// 型パラメータを指定しないと、T は unknown になる可能性がある
const container = new Container(\"hello\");
// container.value の型が推論できない場合がある

解決方法: デフォルト型パラメータを設定します。

class Container<T = string> {
  value: T;
  constructor(value: T) {
    this.value = value;
  }
}

const container = new Container(\"hello\");
// container.value の型が string と推論される

エラー4: Genericsの過度な使用

問題コード:

// 不要に複雑なGenrics定義
function process<T, U, V, W, X>(
  a: T,
  b: U,
  c: V,
  d: W,
  e: X
): [T, U, V, W, X] {
  return [a, b, c, d, e];
}

解決方法: Genericsは必要な場合のみ使用し、過度に複雑にしないようにします。

// シンプルで読みやすい定義
function createTuple<T extends readonly unknown[]>(...items: T): T {
  return items;
}

const result = createTuple(\"hello\", 42, true);
// result の型は [string, number, boolean]と推論される

エラー5: Union型での型安全性の喪失

エラー例:

function getValue<T extends string | number>(value: T): string {
  // エラー: Type 'T' is not assignable to type 'string'
  return value.toLowerCase();
}

解決方法: 型ガードまたは条件型を使用します。

function getValue<T extends string | number>(value: T): string {
  if (typeof value === \"string\") {
    return value.toLowerCase();
  }
  return value.toString();
}

Genericsの高度な使い方

条件型(Conditional Types)

// 型パラメータの型に応じて、異なる型を返す
type IsString<T> = T extends string ? true : false;

type A = IsString<\"hello\">; // true
type B = IsString<number>; // false

// より実用的な例
type Flatten<T> = T extends Array<infer U> ? U : T;

type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number

mapped型

// すべてのプロパティを読み取り専用にする
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

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

type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }

ベストプラクティス

1. 明確な型パラメータ名を使用する

// 悪い例
function process<T, U, V>(a: T, b: U, c: V): T {
  return a;
}

// 良い例
function process<Input, Output, Context>(
  input: Input,
  transform: (i: Input) => Output,
  ctx: Context
): Output {
  return transform(input);
}

2. 制約を活用する

// 制約なし(危険)
function getLength<T>(item: T): number {
  return (item as any).length; // any キャストが必要
}

// 制約あり(安全)
function getLength<T extends { length: number }>(item: T): number {
  return item.length; // キャスト不要

3. デフォルト型パラメータを使用する

interface ApiResponse<T = unknown, E = Error> {
  success: boolean;
  data?: T;
  error?: E;
}

type Response1 = ApiResponse; // { success: boolean; data?: unknown; error?: Error; }
type Response2 = ApiResponse<User>; // { success: boolean; data?: User; error?: Error; }

まとめ

TypeScript Genericsは、型安全性を保ちながらコードの再利用性を高める強力な機能です。本記事で紹介した主なポイントは以下の通りです。

  • Genericsの基本: 型パラメータを使用して、関数、クラス、インターフェースを汎用的に定義できます。
  • 型制約の活用: extendsキーワードを使用することで、型パラメータに制約を加え、より安全な型定義ができます。
  • 複数の型パラメータ: 複数の型を扱う際に、複数の型パラメータを定義することで柔軟な実装が可能になります。
  • よくあるエラーへの対応: 型の不一致、存在しないプロパティへのアクセスなどのエラーは、正しい制約設定と型推論の理解で回避できます。
  • 高度な使い方: 条件型やmapped型を組み合わせることで、さらに複雑で柔軟な型定義が可能になります。

Genericsを正しく理解して使用することで、保守性の高い、スケーラブルなTypeScriptコードを書くことができます。最初は基本的な使い方から始めて、徐々に高度な技法を習得していくことをお勧めします。

タイトルとURLをコピーしました