メインコンテンツまでスキップ

ジェネリクスの基礎

この章では、TypeScriptの代表的な機能の 1 つであるジェネリクス(Generics)について学びます。

この章で学ぶこと

  • ジェネリクスとは何か、なぜ必要か
  • ジェネリック関数の書き方
  • 型パラメータの使い方
  • 型推論の仕組み
備考

ジェネリクスを理解すると、再利用可能で型安全なコードが書けるようになります。最初は難しく感じるかもしれませんが、実際のコード例を通じて理解を深めていきましょう。

ジェネリクスとは

ジェネリクスは、型を「パラメータ」として受け取る仕組みです。これにより、様々な型に対して動作する再利用可能なコンポーネントを作成できます。

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

まず、ジェネリクスを使わない場合の問題を見てみましょう。

// 型ごとに同じロジックの関数を書く必要がある
function getFirstNumber(arr: number[]): number {
return arr[0];
}

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

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

// 使用例
console.log(getFirstNumber([1, 2, 3])); // 1
console.log(getFirstString(["a", "b"])); // "a"
console.log(getFirstBoolean([true, false])); // true

問題点:

  1. 同じロジック(配列の最初の要素を返す)を型ごとに書く必要がある
  2. 新しい型に対応するたびに新しい関数が必要
  3. コードの重複が増える

any型を使った場合の問題

// any型を使うと型安全性が失われる
function getFirstAny(arr: any[]): any {
return arr[0];
}

const num = getFirstAny([1, 2, 3]);
// numはany型 → 型情報が失われる
// num.toUpperCase(); // 実行時エラー(数値にtoUpperCaseはない)

問題点:

  • 戻り値がany型になり、型チェックが効かない
  • 型安全性が失われ、バグの原因になる

ジェネリクスによる解決

// ジェネリック関数
// <T>は型パラメータ(呼び出し時に具体的な型が決まる)
function getFirst<T>(arr: T[]): T {
return arr[0];
}

// 型パラメータを明示的に指定
const num = getFirst<number>([1, 2, 3]); // number型
const str = getFirst<string>(["a", "b"]); // string型
const bool = getFirst<boolean>([true, false]); // boolean型

// 型推論により、型パラメータを省略可能
const num2 = getFirst([1, 2, 3]); // number型(推論される)
const str2 = getFirst(["a", "b"]); // string型(推論される)

メリット:

  • 1つの関数であらゆる型に対応
  • 型安全性が保たれる
  • コードの重複がなくなる

ジェネリック関数を書く3ステップ

具体的な型から始めて、段階的にジェネリック関数に変換する方法を学びましょう。

// ステップ1: まず具体的な型で動作する関数を書く
function getFirstNumber(arr: number[]): number | undefined {
return arr[0];
}

// ステップ2: 型の部分を「型パラメータ」に置き換える
// number → T に置き換え
// 関数名の後に <T> を追加
function getFirstGeneric<T>(arr: T[]): T | undefined {
return arr[0];
}

// ステップ3: 使ってみて型推論を確認
const num = getFirstGeneric([1, 2, 3]); // number | undefined ✓
const str = getFirstGeneric(["a", "b", "c"]); // string | undefined ✓
const obj = getFirstGeneric([{ id: 1 }]); // { id: number } | undefined ✓

ポイント:

  1. まず動くコードを書く: 具体的な型(number, string等)で関数を実装
  2. 型を抽象化: 具体的な型をTに置き換え、関数に<T>を追加
  3. 型推論を確認: 呼び出し時に正しく型が推論されることを確認
備考

「どこをTに置き換えるか迷う」場合は、入力と出力で同じ型であるべき部分を探しましょう。例えば「配列の要素を返す」なら、入力(配列の要素型)と出力(戻り値)は同じであるべきです。

ジェネリック関数

基本的な構文

// 関数宣言の場合
function 関数名<型パラメータ>(引数:): 戻り値の型 {
// 処理
}

// アロー関数の場合
const 関数名 = <型パラメータ>(引数:): 戻り値の型 => {
// 処理
};

identity関数(基本例)

// 受け取った値をそのまま返す関数
function identity<T>(value: T): T {
return value;
}

// 使用例
const num = identity<number>(42); // number型
const str = identity<string>("Hello"); // string型
const obj = identity<{ name: string }>({ name: "太郎" }); // { name: string }型

// 型推論を使用
const num2 = identity(42); // number型(推論)
const str2 = identity("Hello"); // string型(推論)

配列を扱う関数

// 配列の最後の要素を取得
function getLast<T>(arr: T[]): T | undefined {
if (arr.length === 0) {
return undefined;
}
return arr[arr.length - 1];
}

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

// 配列を逆順にする
function reverse<T>(arr: T[]): T[] {
return [...arr].reverse();
}

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

値を配列でラップする

// 値を配列に包む
function wrapInArray<T>(value: T): T[] {
return [value];
}

console.log(wrapInArray(42)); // [42]
console.log(wrapInArray("hello")); // ["hello"]
console.log(wrapInArray({ x: 1 })); // [{ x: 1 }]

型パラメータ

型パラメータの命名規則

型パラメータには慣習的に単一の大文字が使われます。

パラメータ名意味使用例
TType(型)一般的な型
KKey(キー)オブジェクトのキー
VValue(値)オブジェクトの値
EElement(要素)配列の要素
RResult(結果)戻り値の型
// 複数の型パラメータを使う例
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}

const p1 = pair<number, string>(1, "one"); // [number, string]
const p2 = pair<string, boolean>("flag", true); // [string, boolean]

// 型推論を使用
const p3 = pair(42, "answer"); // [number, string]

複数の型パラメータ

// キーと値のペアを作成
function createEntry<K, V>(key: K, value: V): { key: K; value: V } {
return { key, value };
}

const entry1 = createEntry("name", "太郎");
// { key: string; value: string }

const entry2 = createEntry(1, { name: "太郎" });
// { key: number; value: { name: string } }

// マップ関数
function map<T, R>(arr: T[], fn: (item: T) => R): R[] {
return arr.map(fn);
}

const numbers = [1, 2, 3];
const doubled = map(numbers, n => n * 2); // number[]
const strings = map(numbers, n => n.toString()); // string[]

型推論

TypeScriptは引数から型パラメータを自動的に推論できます。

基本的な型推論

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

// 型パラメータを明示
const a = identity<number>(42);

// 型推論(引数から推論される)
const b = identity(42); // Tはnumberと推論

// 複数の型パラメータでも推論
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}

const c = pair(42, "hello");
// Tはnumber、Uはstringと推論

型推論が効く場合と効かない場合

// 型推論が効く: 引数から型が推論できる
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
const first = getFirst([1, 2, 3]); // Tはnumberと推論

// 型推論が効かない: 引数に型情報がない場合
function createEmpty<T>(): T[] {
return [];
}
// const arr = createEmpty(); // エラーではないが T は unknown と推論されてしまう(unknown[])
const arr = createEmpty<string>(); // 明示的に指定が必要

コールバック関数での型推論

// コールバック関数の型推論
function processArray<T, R>(arr: T[], processor: (item: T) => R): R[] {
return arr.map(processor);
}

const numbers = [1, 2, 3];

// processorの引数itemはnumber型と推論される
const doubled = processArray(numbers, item => item * 2);
// item: number, 戻り値: number[]

const strings = processArray(numbers, item => `Number: ${item}`);
// item: number, 戻り値: string[]

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

基本構文

// アロー関数でのジェネリクス
const identity = <T>(value: T): T => {
return value;
};

// 複数の型パラメータ
const pair = <T, U>(first: T, second: U): [T, U] => {
return [first, second];
};

TSX(React)での注意点

// TSXファイルでは<T>がJSXタグと解釈される可能性
// カンマをつけて回避
const identity = <T,>(value: T): T => {
return value;
};

// または extends を使用
const identity2 = <T extends unknown>(value: T): T => {
return value;
};

オブジェクトのメソッドでのジェネリクス

const utils = {
// メソッドでジェネリクスを使用
toArray<T>(value: T): T[] {
return [value];
},

repeat<T>(value: T, count: number): T[] {
return Array(count).fill(value);
},

map<T, R>(arr: T[], fn: (item: T) => R): R[] {
return arr.map(fn);
}
};

console.log(utils.toArray(42)); // [42]
console.log(utils.toArray("hello")); // ["hello"]
console.log(utils.repeat("x", 5)); // ["x", "x", "x", "x", "x"]
console.log(utils.map([1, 2, 3], n => n * 2)); // [2, 4, 6]

実践例:汎用的な配列操作

// フィルター関数
function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
const result: T[] = [];
for (const item of arr) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}

const numbers = [1, 2, 3, 4, 5];
const evens = filter(numbers, n => n % 2 === 0); // [2, 4]

const words = ["apple", "banana", "cherry"];
const longWords = filter(words, w => w.length > 5); // ["banana", "cherry"]

// 検索関数
function find<T>(arr: T[], predicate: (item: T) => boolean): T | undefined {
for (const item of arr) {
if (predicate(item)) {
return item;
}
}
return undefined;
}

const found = find(numbers, n => n > 3); // 4

// グループ化関数
function groupBy<T, K extends string | number>(
arr: T[],
getKey: (item: T) => K
): Record<K, T[]> {
const result = {} as Record<K, T[]>;
for (const item of arr) {
const key = getKey(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
}
return result;
}

const people = [
{ name: "太郎", age: 20 },
{ name: "花子", age: 20 },
{ name: "次郎", age: 30 }
];

const byAge = groupBy(people, p => p.age);
// { 20: [{name: "太郎"...}, {name: "花子"...}], 30: [{name: "次郎"...}] }

試してみよう: 汎用的な配列操作関数を作ろう ★★

以下の関数を実装してください。

要件:

  1. unique<T>: 配列から重複を除去した新しい配列を返す
  2. chunk<T>: 配列を指定したサイズで分割した2次元配列を返す
  3. zip<T, U>: 2つの配列を組み合わせてタプルの配列を返す
// 以下の関数を実装してください
// function unique<T>(arr: T[]): T[]
// function chunk<T>(arr: T[], size: number): T[][]
// function zip<T, U>(arr1: T[], arr2: U[]): [T, U][]
ヒント
  1. unique: Setを使うと重複を除去できます
  2. chunk: ループでsliceを使って配列を分割します
  3. zip: 短い方の配列の長さを基準にします
回答と解説
// 重複を除去
function unique<T>(arr: T[]): T[] {
return [...new Set(arr)];
}

// 配列を指定サイズで分割
function chunk<T>(arr: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}

// 2つの配列を結合
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
const length = Math.min(arr1.length, arr2.length);
const result: [T, U][] = [];
for (let i = 0; i < length; i++) {
result.push([arr1[i], arr2[i]]);
}
return result;
}

// テスト
console.log(unique([1, 2, 2, 3, 3, 3]));
// [1, 2, 3]

console.log(unique(["a", "b", "a", "c"]));
// ["a", "b", "c"]

console.log(chunk([1, 2, 3, 4, 5], 2));
// [[1, 2], [3, 4], [5]]

console.log(chunk(["a", "b", "c", "d"], 3));
// [["a", "b", "c"], ["d"]]

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

console.log(zip([1, 2], ["a", "b", "c", "d"]));
// [[1, "a"], [2, "b"]](短い方の長さで切られる)

解説:

  • unique: Setは重複を自動で除去します。スプレッド構文で配列に戻します
  • chunk: slice(i, i + size)で配列の一部を切り出し、結果配列に追加していきます
  • zip: 2つの配列の短い方の長さを基準にして、対応する要素をタプルにまとめます

これらの関数はジェネリクスにより、どんな型の配列でも型安全に使用できます。

まとめ

この章で学んだこと:

  • ジェネリクスとは: 型をパラメータとして受け取る仕組み
  • なぜ必要か: 型安全性を保ちながらコードを再利用できる
  • ジェネリック関数: <T>で型パラメータを定義
  • 型パラメータ: T, K, Vなどの命名規則
  • 型推論: 引数から型パラメータが自動的に推論される

次の章では、ジェネリクスの制約やジェネリッククラスについて学びます。

初学者がつまずきやすいポイント

警告

よくある間違い

❌ 型パラメータに具体的な型を使ってしまう

// ❌ 型パラメータに具体的な型名を使う
function identity<string>(value: string): string {
return value; // これは型パラメータ"string"であり、string型ではない
}

// ✅ 慣習的な名前を使う
function identityCorrect<T>(value: T): T {
return value;
}

原因: <string>は「stringという名前の型パラメータ」であり、組み込みのstring型ではありません。 解決策: 型パラメータにはT, U, K, Vなどの慣習的な名前を使いましょう。

❌ 型推論に頼りすぎて意図しない型になる

// ❌ 空配列の型が推論できない
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}

const result = getFirst([]); // T は never と推論され、結果は never | undefined

// ✅ 型パラメータを明示
const result2 = getFirst<string>([]); // string | undefined

原因: 空配列からは要素の型を推論できないため、never型になります。 解決策: 型を推論できない場合は、型パラメータを明示的に指定しましょう。

❌ ジェネリクス内で型パラメータのプロパティにアクセス

// ❌ Tに特定のプロパティがあるとは限らない
function getLength<T>(value: T): number {
return value.length; // Error: Property 'length' does not exist on type 'T'
}

// ✅ 制約を追加する(次章で詳しく学びます)
function getLengthSafe<T extends { length: number }>(value: T): number {
return value.length; // OK
}

原因: 型パラメータTは何でもあり得るため、特定のプロパティの存在は保証されません。 解決策: extendsで制約を追加するか、型パラメータを使わない設計を検討しましょう。

❌ TSXでアロー関数のジェネリクスがエラーになる

// ❌ TSXファイルで<T>がJSXと誤認される
const identity = <T>(value: T): T => value;
// Error: JSX element 'T' has no corresponding closing tag.

// ✅ 方法1: カンマを追加
const identity1 = <T,>(value: T): T => value;

// ✅ 方法2: extendsを追加
const identity2 = <T extends unknown>(value: T): T => value;

原因: TSXファイルでは<T>がJSXタグとして解釈されます。 解決策: <T,>のようにカンマを追加するか、<T extends unknown>のように制約を追加しましょう。

❌ 複数の型パラメータで関係性を定義し忘れる

// ❌ 関係性がないため型安全でない
function setProperty<T, K, V>(obj: T, key: K, value: V): void {
(obj as any)[key as any] = value; // 型安全でない
}

// ✅ 型パラメータ間に関係性を定義
function setPropertySafe<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}

const user = { name: "太郎", age: 30 };
setPropertySafe(user, "name", "次郎"); // OK
// setPropertySafe(user, "name", 123); // Error: stringが必要

原因: 型パラメータ同士に関係がないと、型チェックが機能しません。 解決策: K extends keyof TT[K]のように、型パラメータ間の関係を定義しましょう。


const 型パラメータ(TS 5.0+)

通常、ジェネリック関数に配列やオブジェクトリテラルを渡すと、型パラメータは「幅広い型」として推論されます(例: string[])。<const T> と宣言すると、値を as const 付きで渡したのと同じ効果が得られ、リテラル型を保持できます。

// ❌ 通常のジェネリクス: T は string[] と推論される
function createList<T>(items: T[]): T[] {
return items
}

const list1 = createList(['a', 'b', 'c'])
// list1 の型: string[]

// ✅ const 型パラメータ: T はリテラル型として推論される
function createConstList<const T>(items: T[]): T[] {
return items
}

const list2 = createConstList(['a', 'b', 'c'])
// list2 の型: readonly ['a', 'b', 'c']

実用例: タプル型の推論

// タプル要素の型を保ったまま受け取りたい関数
function tuple<const T extends readonly unknown[]>(...args: T): T {
return args
}

const pair = tuple('hello', 42)
// pair の型: readonly ['hello', 42]
// 従来は [string, number] に広がっていた
備考

<const T> は呼び出し側が as const を書かなくても同じ効果を得られるため、ライブラリ API を設計するときに特に有用です。一方、内部でミュータブルに扱う必要がある場合は、従来通りの <T> を使います。

備考

型パラメータのデフォルト値について

型パラメータにはデフォルト値を持たせることもできます(<T = string> のように)。これは次章(14 章)の制約とあわせて詳しく扱います。


次に読む