次のTypeScriptの関数 function identity<T>(arg: T): T { return arg; } について、正しい説明はどれか。
解説
正解は「Tは呼び出し時に決まる型変数で、引数の型と戻り値の型が同じであることを保証する」です。ジェネリックの本質は型を変数のように扱える仕組みで、identity("hello")と呼べばTはstringに、identity(42)と呼べばTはnumberに推論されます。このとき戻り値の型もTなので、「入力と出力が同じ型である」という関係性がコンパイラに伝わります。anyとの違いはここで、anyにしてしまうと型情報が消えてしまい、戻り値に対して好きなメソッドを呼べる代わりに安全性を失います。実行時チェックではないので選択肢3も誤り、TypeScriptの型は全てコンパイル時に消える(type erasure)ためボクシングも発生しません。anyで書くと何が失われるのかジェネリックの価値は「型の関係性を表現できる」点にあります。たとえばidentityをany版で書くと次のようになります。function identityAny(arg: any): any { return arg; } const x = identityAny("hello"); // x は any 型、x.toFixed() もコンパイルが通ってしまう一方ジェネリック版なら、const y = identity("hello") の y は string に推論され、y.toFixed() はコンパイルエラーになります。つまりジェネリックは「anyの安全な代替」ではなく、呼び出し側の型情報を関数内部や戻り値に引き継ぐための仕組みなのです。どんなときにTを書くべきか「関数の入力と出力、あるいは複数の引数同士に型の繋がりがある」ときがジェネリックの出番です。逆に言うと、繋がりがないなら素直に具体型やunionで書いた方が読みやすくなります。よくある使いどころは次のようなケースです。配列から要素を取り出す関数(first<T>(arr: T[]): T | undefined)オブジェクトのキーに対応する値を返す関数(keyof と組み合わせる)ラッパー型を剥がす・被せる関数(Promiseやコンテナ型)「型の関係を表現したいか、それとも単に色々な型を受け入れたいだけか」を自問するのが、ジェネリックを使うかどうかの判断基準になります。