TypeScriptジェネリクス実践ガイド:日常コードで活用する方法
どんな型でも動く関数を書いているのに、TypeScriptがanyについて文句を言い続ける。あるいは、異なる型を扱うためだけに同じ関数を3回コピーしたことがあるでしょう。ジェネリクスはこれを解決します。複数の型で動作するコードを、型安全性を保ちながら書けるのです。しかし、多くの開発者はジェネリクスを避けたり、逆に使いすぎたりします。このガイドでは、頭痛の種にならない日常的なコードでジェネリクスを使う方法を紹介します。
ジェネリクスとは何か、なぜ気にするべきか?
ジェネリクスは型をパラメータ化する方法です。stringやnumberのような型をハードコードする代わりに、型変数(よくTが使われます)を使い、関数やクラスが使用されるときに具体的な型に置き換えられます。
なぜこれが重要なのでしょうか?型安全を保ちながら再利用可能なコードを書けるからです。ジェネリクスがなければ、コードを重複させるかanyを使うことになり、TypeScriptの目的が台無しになります。
ジェネリック関数:基本の構成要素
簡単な例から始めましょう。配列の最初の要素を返す関数が欲しいとします。
function firstElement(arr: any[]): any {
return arr[0];
}
これは動作しますが、型情報が失われます。文字列の配列を渡しても、戻り値の型はanyであり、stringではありません。次にジェネリクスを使います:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
これでTypeScriptは、string[]を渡すとstring | undefinedが返ることを認識します。型が保持されます。
複数の型パラメータを使うこともできます:
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
ジェネリックインターフェースと型エイリアス
ジェネリクスは関数だけに限りません。柔軟なデータ構造を記述するために、ジェネリックインターフェースや型エイリアスを作成できます。
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
これは特にAPIレスポンスに役立ちます。エンドポイントごとに新しいインターフェースを定義する代わりに、ジェネリックなApiResponse<T>を定義できます:
interface ApiResponse<T> {
data: T;
status: number;
}
type User = { id: number; name: string };
const response: ApiResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200,
};
ジェネリック制約:ルールを追加する
ジェネリクスで使用できる型を制限したい場合があります。そこで制約の出番です。extendsキーワードを使って、型が特定のプロパティを持つことを要求します。
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // OK
getLength([1, 2, 3]); // OK
getLength(42); // Error: number doesn't have length
制約によりジェネリクスがより予測可能になり、実行時エラーを防げます。
ジェネリッククラス
クラスもジェネリックにできます。スタック、キュー、リポジトリなどのデータ構造に便利です。
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined
ジェネリクスを使うべき時(使わないべき時)
ジェネリクスは強力ですが、使いすぎるとコードが読みにくくなります。簡単なガイドを以下に示します:
| ジェネリクスを使うべき時 | ジェネリクスを避けるべき時 |
|---|---|
| 型の関係を保持する必要がある | 単純なユニオン型やanyで十分 |
| 再利用可能なユーティリティを書いている | 型が常に同じ |
| コードの重複を避けたい | コードが理解しにくくなる |
経験則:複数の型に対して同じロジックを書いているなら、ジェネリクスを検討しましょう。3つ目の型パラメータを追加して誰も理解できなくなったら、一歩引いて考えましょう。
一般的なパターンとベストプラクティス
- 型パラメータには説明的な名前を使う。 単純な場合は
Tで十分ですが、TDataやTResponseの方が明確な場合があります。 anyよりも制約を優先する。 柔軟性が必要なら、extendsを使って最小限の形状を強制しましょう。- デフォルト型パラメータを活用する。 デフォルトを提供できます:
<T = string>。 - ユーティリティ型とジェネリクスを組み合わせる。 TypeScriptの組み込み
Partial<T>、Readonly<T>、Record<K, V>はすべてジェネリックです。
実践例:ジェネリックAPIクライアント
実用的な例で締めくくりましょう:データを取得し、型付きレスポンスを返すシンプルなAPIクライアントです。
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
async function main() {
const post = await fetchData<Post>("https://jsonplaceholder.typicode.com/posts/1");
console.log(post.title); // string
}
このパターンはフロントエンドコードでよく見られます。エンドポイントごとに繰り返すことなく型安全性を得られます。
FAQ
TypeScriptの<T>構文は何を意味しますか?
<T>は型パラメータを宣言します。関数、インターフェース、クラスが使用されるときに指定される型のプレースホルダーです。名前は何でも構いませんが、単一の型にはTが慣例です。
複数のジェネリック型パラメータを使えますか?
はい。必要なだけカンマで区切って定義できます:<T, U, V>。コードを読みやすくするために説明的な名前を使いましょう。
ジェネリクスをオブジェクトのみ受け入れるように制約するには?
T extends objectを使います。これによりTがstringやnumberのようなプリミティブではなくオブジェクト型であることが保証されます。T extends { id: number }のように特定の形状に制約することもできます。
ジェネリクスは保守可能なTypeScriptを書くための核心です。JSONデータを扱っていて、素早く検証や整形をしたい場合は、JSON Formatterを使ってAPIレスポンスを整形・確認してみてください。