TL;DR

型をパラメータ(型パラメータ)として受け取り、呼び出し側が指定するさまざまな型で動く関数や型を、1 つの定義で書く仕組み。Go や TypeScript などの言語が備えている。

なぜ必要か

ジェネリクスがないと、同じロジックを型ごとに書き分けることになる。Go の公式チュートリアルでは、int64 を合計する SumInts と float64 を合計する SumFloats を、1 つのジェネリック関数にまとめている。TypeScript の Handbook によると、any を使っても任意の型を受け取れるが、戻り値の型の情報が失われる。ジェネリクスなら、入力の型を型パラメータで捕らえて出力にも使える。

仕組み

関数や型は、通常の引数に加えて型パラメータを宣言できる。呼び出すときは型引数を渡すが、多くの場合はコンパイラが引数の型から推論するので省略できる。

型パラメータに使える型は制約で絞る。Go では制約をインタフェースで書き、comparable や int64 | float64 のような型の和を使える。TypeScript では extends で制約を付ける。ジェネリックなコードの中では、制約が保証する操作しか使えない。

例

type Number interface {
    int64 | float64
}
 
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}
function identity<Type>(arg: Type): Type {
  return arg
}
let output = identity("myString") // string と推論される

注意点

型パラメータは「どんな型でもよい」ものとして扱われるので、制約を付けずにプロパティやメソッドを使うとコンパイルエラーになる。TypeScript では、型パラメータを取る型を引数に取ること(Higher Kinded Types)は今のところ表現できない。

関連

出典