次の identity 関数について、正しい説明はどれですか? function identity<T>(value: T): T { return value; } const a = identity<string>('hello'); const b = identity(42);
解説
正解は 「T は呼び出しごとに決まる型のプレースホルダで、a は string、b は number に推論される」です。<T> は型引数(ジェネリクス)と呼ばれ、関数を呼び出すその時に実際の型が決まる「型の変数」のような存在です。identity<string>('hello') のように明示もできますし、identity(42) のように省略すると引数から自動で T = number と推論されます。あらかじめ決まった型の集合ではありませんし、any とも違って型安全性は保たれています。省略してもエラーにはなりません。ジェネリクスとanyはどう違うのか「どんな型でも受け取れる」という点だけ見ると any と似て見えますが、決定的に違います。function identityAny(value: any): any { return value; } function identityT<T>(value: T): T { return value; } const x = identityAny('hello'); // x は any const y = identityT('hello'); // y は stringany は型情報を捨ててしまうので、戻り値を受け取った側で何でもできてしまい、補完もエラーチェックも効きません。一方ジェネリクスは入力の型を覚えておいて出力にも反映するため、y.toUpperCase() は通るけど y * 2 はエラーになる、といった賢い挙動が得られます。実務でよく見るパターン配列操作: function first<T>(arr: T[]): T | undefined のように、入力配列の要素型をそのまま返り値に伝播させるAPIレスポンス: function fetchJson<T>(url: string): Promise<T> のようにして、呼び出し側で期待する型を注入するReactコンポーネント: useState<User | null>(null) のように初期値から推論できないときに明示する型引数に制約をつける extends「何でもいい」ではなく「特定の形を持つ型だけ受け付けたい」こともあります。その場合は extends で制約を付けます。function getLength<T extends { length: number }>(value: T): number { return value.length; } getLength('hello'); // OK(string は length を持つ) getLength([1, 2, 3]); // OK getLength(42); // Error(number は length を持たない)T という名前は慣習で、意味のある名前(TItem, TResponse など)を付けても構いません。慣れてきたら「この関数、入力と出力の型は連動しているな」と感じた瞬間にジェネリクスを使えるようになります。