はじめに

TypeScriptで開発を進めていると、「さまざまな型に対応できる汎用的な関数を作りたい」「型安全を保ちながら再利用可能なコンポーネントを実装したい」という場面に遭遇します。こうしたニーズに応えるのが、TypeScriptのジェネリクス(Generics)です。

ジェネリクスを使えば、any型に頼ることなく、型の柔軟性と型安全性を両立できます。配列操作やPromise、React Hooksなど、TypeScriptの多くの組み込み機能がジェネリクスで実装されています。

本記事では、TypeScriptジェネリクスの基本構文から、ジェネリック関数、ジェネリッククラス、ジェネリック型エイリアス、そして制約(extends)の使い方まで、段階的に解説します。

この記事を読み終えると、以下のことができるようになります。

  • ジェネリクスの概念と必要性を理解できる
  • ジェネリック関数を定義して型安全に呼び出せる
  • ジェネリッククラスで再利用可能なデータ構造を作成できる
  • ジェネリック型エイリアスとインターフェースを定義できる
  • extendsを使った型制約で型パラメータに条件を付けられる

実行環境・前提条件

前提知識

動作確認環境

ツール バージョン
Node.js 20.x以上
TypeScript 5.7以上
VS Code 最新版

本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。

期待される結果

本記事のコードを実行すると、以下の動作を確認できます。

  • ジェネリック関数が型引数に応じた型推論を行う
  • ジェネリッククラスが型安全にデータを管理する
  • 型制約によって不適切な型引数がコンパイルエラーになる

TypeScriptジェネリクスとは

ジェネリクスとは、「型をパラメータとして受け取る」仕組みです。関数に引数を渡すように、型を渡すことで、同じコードを異なる型で再利用できます。

flowchart LR
    A["ジェネリック関数"] --> B["型パラメータ T"]
    B --> C["T = string として呼び出し"]
    B --> D["T = number として呼び出し"]
    B --> E["T = User として呼び出し"]
    C --> F["string用の処理"]
    D --> G["number用の処理"]
    E --> H["User用の処理"]

ジェネリクスが必要な理由

ジェネリクスがない場合、汎用的な関数を作ろうとするとany型に頼るか、型ごとに関数を定義する必要があります。

1
2
3
4
5
6
7
8
// any型を使う場合(型安全性が失われる)
function identityAny(arg: any): any {
  return arg;
}

const resultAny = identityAny("hello");
// resultAnyの型はany → 型情報が失われている
console.log(resultAny.toUpperCase()); // 実行時まで型エラーがわからない
1
2
3
4
5
6
7
8
// 型ごとに関数を定義する場合(コードの重複)
function identityString(arg: string): string {
  return arg;
}

function identityNumber(arg: number): number {
  return arg;
}

ジェネリクスを使えば、型安全性を保ちながら、一つの関数定義で複数の型に対応できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ジェネリクスを使う場合(型安全で再利用可能)
function identity<T>(arg: T): T {
  return arg;
}

const resultString = identity("hello"); // 型: string
const resultNumber = identity(42);       // 型: number

console.log(resultString.toUpperCase()); // OK
console.log(resultNumber.toFixed(2));    // OK

TypeScriptジェネリック関数の基本

ジェネリック関数は、関数名の後に<T>のような型パラメータを宣言します。この型パラメータを引数や戻り値の型として使用できます。

基本構文

1
2
3
4
// ジェネリック関数の基本構文
function 関数名<型パラメータ>(引数: 型パラメータ): 戻り値型 {
  // 処理
}

identity関数の例

最もシンプルなジェネリック関数は、渡された値をそのまま返すidentity関数です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function identity<T>(arg: T): T {
  return arg;
}

// 明示的に型引数を指定
const output1 = identity<string>("TypeScript");
console.log(output1); // "TypeScript"

// 型推論に任せる(推奨)
const output2 = identity(100);
console.log(output2); // 100
console.log(typeof output2); // "number"

型引数を省略した場合、TypeScriptは渡された引数から型を推論します。コードが簡潔になるため、型推論に任せるのが一般的です。

配列を扱うジェネリック関数

配列要素の型を型パラメータで表現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 配列の最初の要素を返す関数
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}

const firstNumber = getFirst([1, 2, 3]);
console.log(firstNumber); // 1(型: number | undefined)

const firstString = getFirst(["a", "b", "c"]);
console.log(firstString); // "a"(型: string | undefined)

const emptyResult = getFirst([]);
console.log(emptyResult); // undefined

複数の型パラメータ

複数の型パラメータを使って、異なる型の関係を表現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 2つの値をペアにする関数
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result1 = pair("name", 25);
console.log(result1); // ["name", 25](型: [string, number])

const result2 = pair(true, { id: 1 });
console.log(result2); // [true, { id: 1 }](型: [boolean, { id: number }])
1
2
3
4
5
6
7
// キーと値からオブジェクトを作る関数
function createEntry<K extends string, V>(key: K, value: V): { [P in K]: V } {
  return { [key]: value } as { [P in K]: V };
}

const entry = createEntry("userId", 12345);
console.log(entry); // { userId: 12345 }

アロー関数でのジェネリクス

アロー関数でもジェネリクスを使用できます。

1
2
3
4
5
6
7
8
// アロー関数でのジェネリクス
const identityArrow = <T>(arg: T): T => arg;

const value1 = identityArrow("hello");
console.log(value1); // "hello"

const value2 = identityArrow(42);
console.log(value2); // 42

TSXファイル(React)では、<T>がJSXタグと解釈される可能性があるため、以下のように記述します。

1
2
3
4
// TSXファイルでの書き方
const identityTsx = <T,>(arg: T): T => arg;
// または
const identityTsx2 = <T extends unknown>(arg: T): T => arg;

TypeScriptジェネリック関数の実践例

実務でよく使うパターンをいくつか紹介します。

配列操作のユーティリティ関数

1
2
3
4
5
6
7
8
// 配列の最後の要素を取得
function getLast<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

console.log(getLast([1, 2, 3]));       // 3
console.log(getLast(["a", "b"]));      // "b"
console.log(getLast([]));              // undefined
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 配列をシャッフル
function shuffle<T>(arr: T[]): T[] {
  const result = [...arr];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

const numbers = [1, 2, 3, 4, 5];
console.log(shuffle(numbers)); // ランダムな順序(例: [3, 1, 5, 2, 4])
1
2
3
4
5
6
7
// 配列から重複を除去
function unique<T>(arr: T[]): T[] {
  return [...new Set(arr)];
}

console.log(unique([1, 2, 2, 3, 3, 3])); // [1, 2, 3]
console.log(unique(["a", "b", "a"]));    // ["a", "b"]

非同期処理のラッパー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 遅延実行
function delay<T>(ms: number, value: T): Promise<T> {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}

// 使用例
async function main() {
  const result = await delay(1000, "Done!");
  console.log(result); // 1秒後に "Done!"
}

main();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// リトライ処理
async function retry<T>(
  fn: () => Promise<T>,
  maxRetries: number
): Promise<T> {
  let lastError: Error | undefined;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      console.log(`Retry ${i + 1}/${maxRetries}`);
    }
  }
  
  throw lastError;
}

// 使用例
async function fetchData(): Promise<string> {
  // 実際のAPI呼び出しなど
  return "data";
}

retry(fetchData, 3)
  .then(console.log)
  .catch(console.error);

TypeScriptジェネリッククラス

クラスでもジェネリクスを使って、型安全で再利用可能なデータ構造を作成できます。

基本構文

1
2
3
class クラス名<T> {
  // Tを型として使用
}

ジェネリックなスタック

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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;
  }
}

// number型のスタック
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.pop());  // 3
console.log(numberStack.peek()); // 2
console.log(numberStack.size()); // 2

// string型のスタック
const stringStack = new Stack<string>();
stringStack.push("a");
stringStack.push("b");
console.log(stringStack.pop()); // "b"

ジェネリックなキュー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Queue<T> {
  private items: T[] = [];

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

  dequeue(): T | undefined {
    return this.items.shift();
  }

  front(): T | undefined {
    return this.items[0];
  }

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

  size(): number {
    return this.items.length;
  }
}

// タスクのキュー
interface Task {
  id: number;
  name: string;
}

const taskQueue = new Queue<Task>();
taskQueue.enqueue({ id: 1, name: "Task A" });
taskQueue.enqueue({ id: 2, name: "Task B" });

console.log(taskQueue.dequeue()); // { id: 1, name: "Task A" }
console.log(taskQueue.front());   // { id: 2, name: "Task B" }

キーバリューストア

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class KeyValueStore<K, V> {
  private store = new Map<K, V>();

  set(key: K, value: V): void {
    this.store.set(key, value);
  }

  get(key: K): V | undefined {
    return this.store.get(key);
  }

  has(key: K): boolean {
    return this.store.has(key);
  }

  delete(key: K): boolean {
    return this.store.delete(key);
  }

  keys(): K[] {
    return Array.from(this.store.keys());
  }

  values(): V[] {
    return Array.from(this.store.values());
  }
}

// string -> number のストア
const scores = new KeyValueStore<string, number>();
scores.set("Alice", 95);
scores.set("Bob", 87);
console.log(scores.get("Alice")); // 95
console.log(scores.keys());       // ["Alice", "Bob"]

// number -> object のストア
interface User {
  name: string;
  email: string;
}

const users = new KeyValueStore<number, User>();
users.set(1, { name: "Alice", email: "alice@example.com" });
users.set(2, { name: "Bob", email: "bob@example.com" });
console.log(users.get(1)); // { name: "Alice", email: "alice@example.com" }

TypeScriptジェネリック型エイリアスとインターフェース

型エイリアス(type)やインターフェース(interface)でもジェネリクスを使用できます。

ジェネリック型エイリアス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// APIレスポンスの共通型
type ApiResponse<T> = {
  success: boolean;
  data: T;
  error?: string;
};

// User用のレスポンス
interface User {
  id: number;
  name: string;
}

const userResponse: ApiResponse<User> = {
  success: true,
  data: { id: 1, name: "Alice" }
};

console.log(userResponse.data.name); // "Alice"

// 配列用のレスポンス
const usersResponse: ApiResponse<User[]> = {
  success: true,
  data: [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" }
  ]
};

console.log(usersResponse.data.length); // 2

Nullable型

1
2
3
4
5
6
7
8
// nullを許容する型
type Nullable<T> = T | null;

const name: Nullable<string> = "Alice";
const age: Nullable<number> = null;

console.log(name); // "Alice"
console.log(age);  // null

Result型(成功/失敗を表現)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 成功または失敗を表す型
type Result<T, E = Error> = 
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return { ok: false, error: "Division by zero" };
  }
  return { ok: true, value: a / b };
}

const result1 = divide(10, 2);
if (result1.ok) {
  console.log(result1.value); // 5
} else {
  console.log(result1.error);
}

const result2 = divide(10, 0);
if (result2.ok) {
  console.log(result2.value);
} else {
  console.log(result2.error); // "Division by zero"
}

ジェネリックインターフェース

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// リポジトリパターン
interface Repository<T, ID> {
  findById(id: ID): Promise<T | undefined>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<T>;
  delete(id: ID): Promise<boolean>;
}

// User用の実装
interface User {
  id: number;
  name: string;
  email: string;
}

class UserRepository implements Repository<User, number> {
  private users: User[] = [];

  async findById(id: number): Promise<User | undefined> {
    return this.users.find(u => u.id === id);
  }

  async findAll(): Promise<User[]> {
    return [...this.users];
  }

  async save(user: User): Promise<User> {
    const index = this.users.findIndex(u => u.id === user.id);
    if (index >= 0) {
      this.users[index] = user;
    } else {
      this.users.push(user);
    }
    return user;
  }

  async delete(id: number): Promise<boolean> {
    const index = this.users.findIndex(u => u.id === id);
    if (index >= 0) {
      this.users.splice(index, 1);
      return true;
    }
    return false;
  }
}

// 使用例
async function demo() {
  const repo = new UserRepository();
  
  await repo.save({ id: 1, name: "Alice", email: "alice@example.com" });
  await repo.save({ id: 2, name: "Bob", email: "bob@example.com" });
  
  const user = await repo.findById(1);
  console.log(user); // { id: 1, name: "Alice", email: "alice@example.com" }
  
  const allUsers = await repo.findAll();
  console.log(allUsers.length); // 2
}

demo();

TypeScriptジェネリクスの型制約(extends)

型パラメータに制約を付けることで、特定の構造を持つ型のみを受け入れるようにできます。

基本的な制約

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// lengthプロパティを持つ型に制約
function logLength<T extends { length: number }>(arg: T): T {
  console.log(`Length: ${arg.length}`);
  return arg;
}

logLength("hello");      // Length: 5
logLength([1, 2, 3]);    // Length: 3
logLength({ length: 10 }); // Length: 10

// logLength(123); // エラー: number型にはlengthプロパティがない

インターフェースを使った制約

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
interface HasId {
  id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

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

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

const users: User[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" }
];

const products: Product[] = [
  { id: 101, title: "Laptop", price: 1200 },
  { id: 102, title: "Phone", price: 800 }
];

console.log(findById(users, 1));      // { id: 1, name: "Alice" }
console.log(findById(products, 102)); // { id: 102, title: "Phone", price: 800 }

keyofとの組み合わせ

keyof演算子と組み合わせることで、オブジェクトのキーを型安全に扱えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// オブジェクトから指定したキーの値を取得
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
};

const name = getProperty(user, "name");
console.log(name); // "Alice"(型: string)

const id = getProperty(user, "id");
console.log(id); // 1(型: number)

// getProperty(user, "age"); // エラー: "age"はuserのキーではない
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// オブジェクトから複数のキーを取り出す
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => {
    result[key] = obj[key];
  });
  return result;
}

const user = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
  password: "secret"
};

const publicUser = pick(user, ["id", "name", "email"]);
console.log(publicUser);
// { id: 1, name: "Alice", email: "alice@example.com" }

複数の制約

&(交差型)を使って複数の制約を組み合わせられます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
interface HasId {
  id: number;
}

interface HasTimestamp {
  createdAt: Date;
  updatedAt: Date;
}

function updateEntity<T extends HasId & HasTimestamp>(
  entity: T,
  updates: Partial<Omit<T, "id" | "createdAt" | "updatedAt">>
): T {
  return {
    ...entity,
    ...updates,
    updatedAt: new Date()
  };
}

interface Article {
  id: number;
  title: string;
  content: string;
  createdAt: Date;
  updatedAt: Date;
}

const article: Article = {
  id: 1,
  title: "Original Title",
  content: "Original Content",
  createdAt: new Date("2025-01-01"),
  updatedAt: new Date("2025-01-01")
};

const updated = updateEntity(article, { title: "New Title" });
console.log(updated.title);     // "New Title"
console.log(updated.updatedAt); // 現在の日時

TypeScriptジェネリクスのデフォルト型パラメータ

型パラメータにデフォルト値を設定できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// デフォルト型パラメータ
interface Container<T = string> {
  value: T;
}

// 型引数を省略するとstring
const stringContainer: Container = { value: "hello" };
console.log(stringContainer.value); // "hello"

// 明示的に指定することも可能
const numberContainer: Container<number> = { value: 42 };
console.log(numberContainer.value); // 42
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// イベントシステムの例
type EventHandler<T = void> = (event: T) => void;

// 引数なしのハンドラー
const clickHandler: EventHandler = () => {
  console.log("Clicked!");
};

// 引数ありのハンドラー
interface MouseEvent {
  x: number;
  y: number;
}

const mouseMoveHandler: EventHandler<MouseEvent> = (event) => {
  console.log(`Position: (${event.x}, ${event.y})`);
};

clickHandler();
mouseMoveHandler({ x: 100, y: 200 });

TypeScriptジェネリクスの型の全体像

これまで解説した内容を図にまとめます。

graph TB
    A[TypeScriptジェネリクス] --> B[ジェネリック関数]
    A --> C[ジェネリッククラス]
    A --> D[ジェネリック型エイリアス]
    A --> E[ジェネリックインターフェース]
    A --> F[型制約 extends]
    
    B --> B1["function fn&lt;T&gt;(arg: T): T"]
    B --> B2["複数型パラメータ &lt;T, U&gt;"]
    
    C --> C1["class Stack&lt;T&gt;"]
    C --> C2["class Map&lt;K, V&gt;"]
    
    D --> D1["type Result&lt;T&gt;"]
    D --> D2["type Nullable&lt;T&gt; = T | null"]
    
    E --> E1["interface Repository&lt;T, ID&gt;"]
    
    F --> F1["&lt;T extends HasId&gt;"]
    F --> F2["&lt;K extends keyof T&gt;"]
    F --> F3["デフォルト型 &lt;T = string&gt;"]

よくある間違いと解決策

1. 制約なしで型のメソッドを呼び出す

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 間違い: Tにlengthがあるかわからない
function getLength<T>(arg: T): number {
  // return arg.length; // エラー: Property 'length' does not exist on type 'T'
  return 0;
}

// 正解: 制約を追加
function getLengthFixed<T extends { length: number }>(arg: T): number {
  return arg.length; // OK
}

console.log(getLengthFixed("hello")); // 5
console.log(getLengthFixed([1, 2, 3])); // 3

2. 型パラメータとanyの混同

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// anyを使った場合(型情報が失われる)
function processAny(items: any[]): any {
  return items[0];
}

const resultAny = processAny([1, 2, 3]);
// resultAnyはany型 → 型チェックが効かない

// ジェネリクスを使った場合(型情報が保持される)
function processGeneric<T>(items: T[]): T | undefined {
  return items[0];
}

const resultGeneric = processGeneric([1, 2, 3]);
// resultGenericはnumber | undefined型 → 型チェックが効く
console.log(resultGeneric?.toFixed(2)); // "1.00"

3. 不必要なジェネリクス

1
2
3
4
5
6
7
8
9
// 不必要: Tを一度しか使っていない
function logValue<T>(value: T): void {
  console.log(value);
}

// シンプルに書ける
function logValueSimple(value: unknown): void {
  console.log(value);
}

型パラメータは、入力と出力の間で型の関係を保持する必要がある場合に使用します。

まとめ

本記事では、TypeScriptジェネリクスの基礎から実践的な使い方まで解説しました。

重要なポイントを振り返ります。

概念 説明 使用例
ジェネリック関数 型パラメータを受け取る関数 function identity<T>(arg: T): T
ジェネリッククラス 型パラメータを持つクラス class Stack<T>
ジェネリック型エイリアス 再利用可能な型定義 type Result<T> = ...
型制約(extends) 型パラメータに条件を付ける <T extends HasId>
keyof制約 オブジェクトのキーを制約 <K extends keyof T>
デフォルト型パラメータ 型引数省略時のデフォルト <T = string>

ジェネリクスを使いこなすことで、型安全性を保ちながら再利用可能なコードを書けるようになります。最初は難しく感じるかもしれませんが、実際にコードを書きながら慣れていくことをおすすめします。

次回の「TypeScriptジェネリクス実践」では、より高度な制約の使い方や条件付き型について解説します。

参考リンク