TypeScript Generic Type とは?初心者向け完全ガイド|型安全性を実現する方法

TypeScript

TypeScript Generic Type とは?初心者向け完全ガイド|型安全性を実現する方法

はじめに

TypeScriptを使用していて、「関数やクラスを再利用可能にしたいけど、型情報を失いたくない」という課題に直面したことはありませんか?こうした問題を解決するのがGeneric Type(ジェネリック型)です。

Genericsは一度習得すれば、コードの再利用性と型安全性を大幅に向上させる強力な機能です。本記事では、初心者でも理解できるよう、基礎から実践的な使い方まで解説します。

第1章:Generic Type の基本概念

Generic Type とは何か

Generic Type(ジェネリック型)は、型を変数のように扱い、複数の型に対応した汎用的なコンポーネントを作成する機能です。

わかりやすく例えると、「小売店の箱」を想像してください:

  • 箱の形や大きさは同じ
  • 中身は靴、本、食器など何でも入れられる
  • 中身の内容を追跡できる

Genericsも同じように、「構造は同じだけど、扱うデータ型を柔軟に変える」という仕組みです。

実際の問題シーン

以下のコードを見てください。これはGeneric なしの例です:

// ❌ Generic なしの場合
function getFirstElement(arr: any[]): any {
  return arr[0];
}

const num = getFirstElement([1, 2, 3]); // 型が any
const str = getFirstElement(['a', 'b']); // 型が any

// 戻り値の型がanyなので、型補完やエラーチェックが機能しない
num.toFixed(2); // エラーが出ない(実際は数値なので OK)
str.toUpperCase(); // エラーが出ない(実際は文字列なので OK)

上記のコードの問題点は、戻り値の型が any になってしまい、IDEの型補完やTypeScriptの型チェックが機能しないことです。

第2章:原因の説明

型情報が失われる理由

JavaScriptは動的型言語ですが、TypeScriptは静的型言語です。型情報を失うと、以下の問題が発生します:

  1. 型推論が機能しない:IDEのオートコンプリートが使えない
  2. 実行時エラーが増える:コンパイル時に型チェックできない
  3. コード保守性が低下:何の型が返されるか不明確
  4. 再利用性が損なわれる:異なる型に対応するたびに同じコードを書き直す

Generic なしでの複数関数の問題

// ❌ 型ごとに関数を書き直す必要がある
function getFirstNumber(arr: number[]): number {
  return arr[0];
}

function getFirstString(arr: string[]): string {
  return arr[0];
}

function getFirstBoolean(arr: boolean[]): boolean {
  return arr[0];
}

// コードの重複が増える = 保守性が低下

第3章:Generic Type の解決手順

ステップ1:Generic の基本構文を理解する

Generic型は、角括弧 <> を使って、「型を受け取る仮の型」を定義します。

// ✅ Generic の基本形
function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

// T は「Type」の略で、「これからここに何かの型が入る」という意味

ここで重要なのが、T はプレースホルダー(型変数)だということです。使う時に型が決まります。

ステップ2:Generic型を使用する

Generic関数は、以下の2つの方法で使用できます:

方法1:型を明示的に指定

const num = getFirstElement<number>([1, 2, 3]);
const str = getFirstElement<string>(['a', 'b', 'c']);

方法2:型推論に頼る(推奨)

const num = getFirstElement([1, 2, 3]); // T は自動的に number に推論される
const str = getFirstElement(['a', 'b', 'c']); // T は自動的に string に推論される

ステップ3:複数の型パラメータを使う

1つの関数で複数の型を扱うこともできます:

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result = pair<number, string>(42, 'hello');
// result の型は [number, string]

第4章:実践的なコード例

例1:Generic 関数

// ✅ Generic関数の実装
function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

const firstNum = getFirstElement([10, 20, 30]);
console.log(firstNum.toFixed(2)); // 型が number として認識される

const firstStr = getFirstElement(['apple', 'banana']);
console.log(firstStr.toUpperCase()); // 型が string として認識される

例2:Generic インターフェース

// ✅ Generic インターフェースの定義
interface Container<T> {
  value: T;
  getValue(): T;
  setValue(val: T): void;
}

// 数値用の実装
class NumberContainer implements Container<number> {
  value: number = 0;
  
  getValue(): number {
    return this.value;
  }
  
  setValue(val: number): void {
    this.value = val;
  }
}

// 文字列用の実装
class StringContainer implements Container<string> {
  value: string = '';
  
  getValue(): string {
    return this.value;
  }
  
  setValue(val: string): void {
    this.value = val;
  }
}

// 使用例
const numContainer = new NumberContainer();
numContainer.setValue(42);
console.log(numContainer.getValue()); // 42

例3:Generic クラス

// ✅ Generic クラスの実装
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;
  }
}

// 使用例
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2

const stringStack = new Stack<string>();
stringStack.push('hello');
stringStack.push('world');
console.log(stringStack.peek()); // 'world'

例4:Generic 制約(Constraints)

Generic型に「最低限これ以上の型であること」という制約を付けられます:

// ❌ length プロパティを持たない型が渡されるとエラー
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

getLength('hello'); // OK(文字列は length プロパティを持つ)
getLength([1, 2, 3]); // OK(配列は length プロパティを持つ)
getLength(123); // ❌ エラー(数値は length プロパティを持たない)

例5:keyof を使った制約

// ✅ オブジェクトのキーを安全に取得
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Alice', age: 30 };

const name = getProperty(user, 'name'); // OK
const age = getProperty(user, 'age'); // OK
// getProperty(user, 'email'); // ❌ エラー(user に email プロパティはない)

第5章:よくある間違い

間違い1:Generic を使わずに any を使う

// ❌ 間違い:型チェックが機能しない
function process(item: any): any {
  return item;
}

// ✅ 正解:Generic で型安全性を保つ
function process<T>(item: T): T {
  return item;
}

間違い2:制約なしで unsafe なプロパティアクセス

// ❌ 間違い:length プロパティが存在するか保証されていない
function getLengthWrong<T>(item: T): number {
  return item.length; // エラー
}

// ✅ 正解:制約で length プロパティを保証
function getLength<T extends { length: number }>(item: T): number {
  return item.length; // OK
}

間違い3:型パラメータを使い忘れる

// ❌ 間違い:T を定義したが使っていない
function createArray<T>(size: number): any[] {
  return new Array(size);
}

// ✅ 正解:型パラメータを活用する
function createArray<T>(size: number, defaultValue: T): T[] {
  return new Array(size).fill(defaultValue);
}

const numbers = createArray(3, 0); // number[] が返される
const strings = createArray(3, ''); // string[] が返される

間違い4:デフォルト値の指定を忘れる

// ❌ 間違い:T が何の型か不明確
function wrap<T>(value: T): { value: T } {
  return { value };
}

// ✅ 正解:デフォルト型を指定
function wrap<T = string>(value: T): { value: T } {
  return { value };
}

const strWrapped = wrap('hello'); // T = string
const numWrapped = wrap<number>(42); // 明示的に型を指定

第6章:実世界での活用例

API レスポンスの型安全な取得

// ✅ Generic を使った API ハンドラ
interface ApiResponse<T> {
  status: number;
  data: T;
  message: string;
}

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

// 使用例
interface User {
  id: number;
  name: string;
  email: string;
}

const user = await fetchData<User>('/api/users/1');
// user は User 型として扱われる
console.log(user.name); // 型補完が機能する

再利用可能な Reducer

// ✅ Redux 風の Generic Reducer
type Action<T> = {
  type: string;
  payload: T;
};

function createReducer<State, Payload>(
  initialState: State,
  handler: (state: State, payload: Payload) => State
) {
  return (state: State = initialState, action: Action<Payload>): State => {
    return handler(state, action.payload);
  };
}

// 使用例
const counterReducer = createReducer<number, number>(
  0,
  (state, payload) => state + payload
);

第7章:まとめ

Generic Type の要点

TypeScript の Generic Type は以下のような強力な機能です:

  • 再利用性の向上:1つのコンポーネントで複数の型に対応
  • 型安全性の確保:any を使わず、コンパイル時に型チェック
  • IDE サポート:型補完やエラーチェックが機能
  • 保守性の向上:コードの重複を削減

学習のステップ

  1. 基本的な Generic 関数を理解する
  2. Generic インターフェースと クラスに進む
  3. 制約(extends)を学ぶ
  4. 複雑な Generic 型を組み合わせる

実践のコツ

  • any を避ける:Generic で型安全性を保つ
  • 制約を活用する:安全で読みやすいコードになる
  • 型推論に頼る:必要に応じて明示的に型を指定する
  • 段階的に学ぶ:簡単な例から始めて複雑な例に進む

最後に

Generic Type は最初は難しく感じるかもしれませんが、繰り返し使うことで自然に身につきます。本記事で学んだコード例を実際に書いてみることが、最も効果的な学習方法です。

TypeScript を真の力で使いこなすために、Generic Type の習得は必須スキルです。ぜひこの機会にマスターしてください!

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