日常的なコードのためのTypeScriptジェネリクス
配列の最初の要素を返す関数を書いているとします。数値、文字列、オブジェクトで動作し、戻り値の型が入力型と一致することを望みます。ジェネリクスがなければ、anyに頼り、型安全性をすべて失うことになります。そこでTypeScriptジェネリクスの出番です。
ジェネリクスを使うと、特定の型情報を保持しながら、複数の型で動作するコードを書くことができます。このガイドでは、日常的なTypeScriptコードでジェネリクスを使用するための実践的なパターンを探ります。
ジェネリクスとは?
ジェネリクスは型をパラメータ化する方法です。関数がパラメータを取るように、ジェネリクスは型パラメータを取ります。これにより、単一の型ではなく、さまざまな型で動作する再利用可能なコンポーネントを作成できます。
次のidentity関数を考えてみましょう:
function identity<T>(arg: T): T {
return arg;
}
ここで、Tは型変数です。identity(42)を呼び出すと、TypeScriptはTをnumberと推論します。identity("hello")を呼び出すと、Tはstringになります。戻り値の型は常に入力型と一致します。
実践的なジェネリック関数
よくあるシナリオを見てみましょう:値をオブジェクトでラップする関数です。
function wrapInObject<T>(value: T): { value: T } {
return { value };
}
ジェネリクスがなければ、anyやunknownを使わざるを得ず、呼び出し側は型情報を失います。ジェネリクスを使えば、戻り値の型は正確に{ value: T }です。
もう一つの日常的な例は、配列の最後の要素を返す関数です:
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
TypeScriptは配列引数からTを推論するので、last([1, 2, 3])はnumber | undefinedを返します。
ジェネリック制約
ジェネリクスで使用できる型を制限したい場合があります。たとえば、特定の型にのみ存在するプロパティにアクセスする必要があるかもしれません。extendsキーワードを使って制約を追加できます。
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
これでTはnumber型のlengthプロパティを持たなければなりません。これは文字列、配列、およびlengthプロパティを持つ任意のオブジェクトで機能します。
インターフェースと制約を組み合わせることもできます:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
これにより、findByIdに渡されるアイテムがidプロパティを持つことが保証されます。
ジェネリックインターフェースと型エイリアス
ジェネリクスは関数に限定されません。柔軟なデータ構造を作成するために、ジェネリックインターフェースや型エイリアスを定義できます。
interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return { data, status: response.status };
}
これでAPIレスポンスを正確に型付けできます:
interface User {
id: number;
name: string;
}
const userResponse = await fetchData<User>('/api/user');
// userResponse.data is User
ジェネリッククラス
クラスもジェネリックにできます。よくある例はシンプルなスタックです:
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];
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined
Reactコンポーネントにおけるジェネリクス
Reactコンポーネントを構築している場合、ジェネリクスはコンポーネントをより再利用可能にします。たとえば、任意の型のアイテムをレンダリングするリストコンポーネント:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, index) => <li key={index}>{renderItem(item)}</li>)}</ul>;
}
これでListを任意の型で使用でき、renderItemコールバックで完全な型チェックが得られます。
よくある落とし穴とベストプラクティス
- ジェネリクスを過度に使用しない。 関数が特定の型でのみ動作する場合は、その型をそのまま使用してください。ジェネリクスは複雑さを増します。
- 説明的な型パラメータ名を使用する。
Tは一般的ですが、複数のパラメータにはTInput、TOutputのような名前を使用します。 anyよりも制約を優先する。 制約は柔軟性を保ちながら型安全性を提供します。- ジェネリクスは実行時に消去されることを覚えておく。 これらはコンパイル時にのみ存在します。
比較:ジェネリクス vs. any vs. ユニオン型
| アプローチ | 型安全性 | 柔軟性 | ユースケース |
|---|---|---|---|
any |
なし | 高い | JSからの移行、簡単なプロトタイプ |
| ユニオン型 | 良好 | 既知の型に限定 | すべての可能な型がわかっている場合 |
| ジェネリクス | 強い | 高い | 型を保持した再利用可能なコンポーネント |
FAQ
anyの代わりにジェネリクスをいつ使うべきですか?
関数やコンポーネント全体で型情報を保持したいときはいつでもジェネリクスを使用してください。anyは型チェックを無効にしますが、ジェネリクスはそれを維持します。何かを動作させるためにanyに頼っているなら、ジェネリクスでより安全に解決できないか検討してください。
複数の型パラメータを使用できますか?
はい。function pair<K, V>(key: K, value: V): [K, V]のように、複数の型パラメータを持つ関数を定義できます。複数ある場合は説明的な名前を使用してください。
.tsxファイルでアロー関数とジェネリクスはどのように連携しますか?
.tsxファイルでは、<T>はJSXと曖昧になる可能性があります。末尾にカンマを付けます:const identity = <T,>(arg: T): T => arg; またはextends unknownを使用します:<T extends unknown>。
ジェネリクスは、TypeScriptコードをより再利用可能で型安全にする強力な機能です。シンプルなジェネリック関数から始めて、徐々にインターフェースやクラスに適用してください。型安全性を保ちながらJSONデータをフォーマットまたは検証する必要がある場合は、JSON Formatterツールをチェックして、JSONをすばやく検査し整形してください。