TypeScriptのジェネリクスの基礎
ジェネリックはTypeScriptの最も強力な機能であり、完全な型安全性を維持しつつ、「複数の型に対応する」コードを書くことを可能にします。ジェネリックを理解することは、TypeScriptを習得するための重要なステップです。
1. なぜジェネリックが必要なのか?
(1) ジェネリクスがないことによるジレンマ
TYPESCRIPT
// Option 1: Use any — missing type information
function firstAny(arr: any[]): any {
return arr[0];
}
let n = firstAny([1, 2, 3]); // n is any — don't know if it's number
let s = firstAny(["a", "b"]); // s is any — don't know if it's string
// Option 2:Write a function for each type——Code Duplication
function firstNumber(arr: number[]): number { return arr[0]; }
function firstString(arr: string[]): string { return arr[0]; }
// Endless...
(2) 汎用的な解決策
TYPESCRIPT
function first<T>(arr: T[]): T {
return arr[0];
}
let n = first([1, 2, 3]); // T Inferred as number → Back number
let s = first(["a", "b"]); // T Inferred as string → Back string
let b = first([true, false]); // T Inferred as boolean → Back boolean
console.log(n.toFixed(2)); // ✅ TypeScript knows n is number
console.log(s.toUpperCase()); // ✅ TypeScript knows s is string
📌 重要な概念:
<T> は型パラメータです。T が何であるかは、関数が呼び出された際に渡される引数の型によって決まります。これは一度定義されるだけで、複数の型で使用することができ、それぞれのケースについて正確な型情報が得られます。
2. ジェネリック関数
(1) 基本的な構文
TYPESCRIPT
function identity<T>(value: T): T {
return value;
}
// TypeScript Automatic Inference T
let num = identity(42); // T = number
let str = identity("hello"); // T = string
// You can also specify it explicitly T(Sometimes it is necessary to)
let result = identity<string>("hello");
(2) 複数の型パラメータ
TYPESCRIPT
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
let p1 = pair("name", 42); // [string, number]
let p2 = pair(true, [1, 2, 3]); // [boolean, number[]]
(3) ジェネリクスと配列
TYPESCRIPT
function map<T, U>(arr: T[], transform: (item: T) => U): U[] {
return arr.map(transform);
}
let numbers = [1, 2, 3];
let strings = map(numbers, n => `No.${n}`);
// T = number, U = string → Back string[]
console.log(strings); // ["No.1", "No.2", "No.3"]
▶ 例:汎用ユーティリティ関数
TYPESCRIPT
// Filter Array
function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
return arr.filter(predicate);
}
// Searching an Array
function find<T>(arr: T[], predicate: (item: T) => boolean): T | undefined {
return arr.find(predicate);
}
// Array Chunking
function chunk<T>(arr: T[], size: number): T[][] {
let result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}
let nums = [1, 2, 3, 4, 5, 6, 7];
let evens = filter(nums, n => n % 2 === 0);
let found = find(nums, n => n > 5);
let groups = chunk(nums, 3);
console.log("Even number:" + evens); // "Even number:2,4,6"
console.log("Found:" + found); // "Found:6"
console.log("Chunking:" + JSON.stringify(groups)); // "[[1,2,3],[4,5,6],[7]]"
出力:
TEXT
Even number:2,4,6
Found:6
Chunking:[[1,2,3],[4,5,6],[7]]
3. ジェネリックインターフェース
(1) 基本的な構文
TYPESCRIPT
interface Box<T> {
value: T;
}
let stringBox: Box<string> = { value: "hello" };
let numberBox: Box<number> = { value: 42 };
(2) 汎用インターフェース記述機能
TYPESCRIPT
interface Transformer<A, B> {
(input: A): B;
}
let toString: Transformer<number, string> = (n) => String(n);
let toLength: Transformer<string, number> = (s) => s.length;
console.log(toString(42)); // "42"
console.log(toLength("hello")); // 5
(3) 汎用インターフェース記述リポジトリパターン
TYPESCRIPT
interface Repository<T> {
findById(id: string): T | null;
findAll(): T[];
save(item: T): void;
delete(id: string): boolean;
}
interface User {
id: string;
name: string;
}
class UserRepo implements Repository<User> {
private data: User[] = [];
findById(id: string): User | null {
return this.data.find(u => u.id === id) ?? null;
}
findAll(): User[] {
return this.data;
}
save(item: User): void {
this.data.push(item);
}
delete(id: string): boolean {
let len = this.data.length;
this.data = this.data.filter(u => u.id !== id);
return this.data.length < len;
}
}
4. ジェネリッククラス
(1) 基本的な構文
TYPESCRIPT
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];
}
get size(): number {
return this.items.length;
}
}
let numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.push(3);
console.log(numStack.pop()); // 3
console.log(numStack.peek()); // 2
console.log(numStack.size); // 2
let strStack = new Stack<string>();
strStack.push("a");
strStack.push("b");
console.log(strStack.pop()); // "b"
(2) ジェネリッククラスの静的メンバ
TYPESCRIPT
class GenericClass<T> {
// static defaultValue: T; // ❌ Static members cannot reference a class's type parameters
// ✅ Static methods require their own type parameters.
static create<U>(value: U): GenericClass<U> {
let instance = new GenericClass<U>();
instance.value = value;
return instance;
}
constructor(public value: T) {}
}
let instance = GenericClass.create("hello"); // GenericClass<string>
📌 理由: クラスの型パラメータ
T はインスタンスレベルのものであり、各インスタンスは異なる T を持つことができます。静的メンバーはクラス自体に属しており、特定のインスタンスに属するものではないため、インスタンスの型パラメータを参照することはできません。
5. ジェネリック制約(extends)
デフォルトでは、ジェネリック型 T は任意の型をとることができますが、場合によってはこれが広すぎることもあります。extends を使用すると、T が特定の条件を満たすように制約を課すことができます:
(1) 特定のインターフェースに限定される
TYPESCRIPT
interface HasLength {
length: number;
}
// T Must have length Properties
function logLength<T extends HasLength>(value: T): void {
console.log(`Length:${value.length}`);
}
logLength("hello"); // ✅ string has length
logLength([1, 2, 3]); // ✅ The array contains length
logLength({ length: 10 }); // ✅ Objects include length
// logLength(42); // ❌ number None length
(2) 制約もまた、汎用的な概念の一つである
TYPESCRIPT
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let user = { name: "Charlie", age: 20, email: "xiao@example.com" };
console.log(getProperty(user, "name")); // ✅ "Charlie" —— K = "name"
console.log(getProperty(user, "age")); // ✅ 20 —— K = "age"
// getProperty(user, "phone"); // ❌ "phone" No user the key
(3) 制約としてのコンストラクタ
TYPESCRIPT
function createInstance<T>(Constructor: new () => T): T {
return new Constructor();
}
class Dog {
bark() { return "Woof!"; }
}
let dog = createInstance(Dog);
console.log(dog.bark()); // "Woof!"
▶ 例:型安全なオブジェクトのマージ
TYPESCRIPT
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
let defaults = { host: "localhost", port: 3000 };
let custom = { port: 8080, debug: true };
let config = merge(defaults, custom);
// Inference Types:{ host: string; port: number } & { port: number; debug: boolean }
// After simplification:{ host: string; port: number; debug: boolean }
console.log(config.host); // "localhost"
console.log(config.port); // 8080
console.log(config.debug); // true
6. 一般的な組み込みジェネリック型
TypeScript には、多くの便利なジェネリックユーティリティ型が含まれています:
| タイプ | 機能 | 例 |
|---|---|---|
Array<T> |
配列型 | Array<number> |
Promise<T> |
プロミス型 | Promise<string> |
Record<K, V> |
キー・値ペアオブジェクト | Record<string, number> |
Partial<T> |
すべての属性は任意です | Partial<Config> |
Required<T> |
すべての項目は必須です | Required<Config> |
Readonly<T> |
すべてのプロパティは読み取り専用です | Readonly<Config> |
Pick<T, K> |
特定の属性を選択 | Pick<User, "name" | "age"> |
Omit<T, K> |
特定の属性を除外する | Omit<User, "email"> |
Exclude<T, U> |
ユニオン型から除外 | Exclude<"a"|"b"|"c", "a"> |
Extract<T, U> |
ユニオン型からの抜粋 | Extract<"a"|"b"|"c", "a"|"b"> |
ReturnType<T> |
関数の戻り値の型 | ReturnType<typeof fn> |
Parameters<T> |
関数引数の型のタプル | Parameters<typeof fn> |
TYPESCRIPT
// Examples of Actual Use
interface User {
id: number;
name: string;
email: string;
age: number;
}
// When creating a user id Not required
type CreateUser = Omit<User, "id">;
// All fields are optional when updating a user
type UpdateUser = Partial<User>;
// The user summary includes only some fields
type UserSummary = Pick<User, "id" | "name">;
let newUser: CreateUser = { name: "Charlie", email: "xiao@example.com", age: 20 };
let update: UpdateUser = { age: 21 };
let summary: UserSummary = { id: 1, name: "Charlie" };
❓ よくある質問
Q ジェネリック型パラメータにはどのような名前を付けるべきですか?
A 慣例として、大文字の単一の文字を使用します。T(Type)、U/V/W(それ以降の型)、K(Key)、V(Value)、E(Element)などです。複数の単語からなる場合は、PascalCaseを使用します:
TItem、TResponse。チームは一貫した命名規則を採用する必要があります。Q ジェネリック型を明示的に指定する必要があるのはいつですか?
A ほとんどの場合、TypeScript はパラメータからジェネリック型を推論できます。以下の状況では、ジェネリック型を明示的に指定する必要があります:(1) 関数にパラメータがない場合、またはパラメータの型から T を特定できない場合;(2) 推論の結果が広すぎる場合(例:ユニオン型として推論される場合); (3) 関数の戻り値の型をより厳密に制御する必要がある場合。
Q ジェネリック制約
extends とクラス継承 extends の違いは何ですか?A 構文は同じですが、意味合いが異なります。ジェネリック制約
T extends HasLength は「T は少なくとも HasLength の構造を持たなければならない」という意味であり、これは型の互換性関係です。クラス継承 class Dog extends Animal は、「Dog は Animal のサブクラスである」という意味であり、これはオブジェクト指向の関係です。Q
Record<string, number> と { [key: string]: number } の違いは何ですか?A これらは完全に同等です。
Record は組み込みの汎用ユーティリティ型であり、本質的にはインデックスシグネチャです。Record<string, number> はより簡潔ですが、{ [key: string]: number } はより柔軟性が高く(既知のプロパティを追加することができます)。📖 まとめ
- ジェネリクス
<T>は型パラメータであり、型安全性を維持しつつ、関数、インターフェース、およびクラスが複数の型に対応できるようにするものです。 - ジェネリック関数:
function fn<T>(arg: T): T。ここで、T は呼び出し時に引数から推論される。 - ジェネリックインターフェースとジェネリッククラス:名前の末尾に「T」を追加する、
interface Box<T>、class Stack<T> - ジェネリック制約
T extends typeは、T が特定の条件を満たすことを保証するために、T の範囲を制限する - TypeScript には、Record、Partial、Required、Readonly、Pick、Omit など、豊富な組み込みのジェネリックユーティリティ型が用意されています。
📝 練習問題
- 基本問題(難易度 ⭐):単一の値を配列にまとめる汎用関数
wrapInArray<T>(value: T): T[]を作成してください。numberおよびstring型でテストしてください。 - 上級問題(難易度 ⭐⭐):オブジェクトの配列から指定されたプロパティの値を抽出する汎用関数
pluck<T, K extends keyof T>(items: T[], key: K): T[K][]を実装してください。例えば、pluck([{name: "a", age: 1}, {name: "b", age: 2}], "name")は["a", "b"]を返します。 - 課題(難易度:⭐⭐⭐):save、find、delete メソッドを提供するジェネリッククラス
DataStore<T extends { id: string }>を実装してください。ジェネリック制約を通じて、id プロパティが存在することを保証してください。CRUD 機能をテストするために、DataStore<{ id: string; title: string }>のインスタンスを作成してください。



