ジェネリクスの基礎
この章では、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
問題点:
- 同じロジック(配列の最初の要素を返す)を型ごとに書く必要がある
- 新しい型に対応するたびに新しい関数が必要
- コードの重複が増える
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 ✓
ポイント:
- まず動くコードを書く: 具体的な型(number, string等)で関数を実装
- 型を抽象化: 具体的な型を
Tに置き換え、関数に<T>を追加 - 型推論を確認: 呼び出し時に正しく型が推論されることを確認
「どこを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 }]
型パラメータ
型パラメータの命名規則
型パラメータには慣習的に単一の大文字が使われます。
| パラメータ名 | 意味 | 使用例 |
|---|---|---|
T | Type(型) | 一般的な型 |
K | Key(キー) | オブジェクトのキー |
V | Value(値) | オブジェクトの値 |
E | Element(要素) | 配列の要素 |
R | Result(結果) | 戻り値の型 |
// 複数の型パラメータを使う例
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: "次郎"...}] }
試してみよう: 汎用的な配列操作関数を作ろう ★★
以下の関数を実装してください。
要件:
unique<T>: 配列から重複を除去した新しい配列を返すchunk<T>: 配列を指定したサイズで分割した2次元配列を返す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][]
ヒント
unique:Setを使うと重複を除去できますchunk: ループでsliceを使って配列を分割します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 TやT[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 章)の制約とあわせて詳しく扱います。