™️

TSでたまに見る<T>とはなにか(ジェネリクス)

2024/12/07に公開

Typescriptを扱ったり、勉強するとたまに見かける<T>。
いつも何使うのか不思議には思っていましたが、調べることがなく、月日が流れていました。
ですので、謎の存在と思っている<T>の正体に迫ります。

<T>はジェネリクス

結論から言うと、**<T>はジェネリクス(Generics)**です。
そもそもジェネリクスと言われても、自分には最初ピンと来ていませんでした。
ジェネリクスとは、型の安全性とコードの共通化の両立ができ、
型も変数のように扱えるようにするものです。
例としては下記です。

const numbers: Array<number> = [1, 2, 3, 4];

or

const numbers: number[] = [1, 2, 3, 4];

上記だと見たことがある人も多いと思います。
この型を抽象化する方法がジェネリクスです。
実際に簡単な例を出します。
※サバイバルTypecScriptからの例を参考しています

通常の書き方

chooseRandomlyString()という普通の関数があります。
この関数は、2つの文字列を引数に受け取り、五分五分の確率で第1引数か第2引数の値を抽選して返します。

function chooseRandomlyString(v1: string, v2: string): string {
  return Math.random() <= 0.5 ? v1 : v2;
}

ただこれ以外にも、数字を返す関数が欲しい!URLを返す関数が欲しい!と3種類できてしまいました。
返り値は異なっています。

// 重複した3つの関数
function chooseRandomlyString(v1: string, v2: string): string {
  return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomlyNumber(v1: number, v2: number): number {
  return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomlyURL(v1: URL, v2: URL): URL {
  return Math.random() <= 0.5 ? v1 : v2;
}

でもこのままではいけません。
returnの処理は同じなので、共通化したいですよね。
でも型が違うのでどうしましょう。
アイデアとしてはanyにする方法があります

function chooseRandomly(v1: any, v2: any): any {
  return Math.random() <= 0.5 ? v1 : v2;
}
let str = chooseRandomly(0, 1);
str = str.toLowerCase(

ただこれだと、コンパイラのチェックが行われなくなり、バグを生みやすくなることです。
型の安全性が損なわれてしまいます。
それはそうですよね。コンパイラエラーで、早めにエラーを知りたいですし、
anyだと何でも受け付けてしまうので、型の機能を活かしたいです。
コンパイル後のコードを実行してみると5行目で「TypeError: str.toLowerCase is not a function」というエラーが発生します。
イメージをするために、下記のデータ構造でデータがやってきています。

function chooseRandomly<String>(v1: <string>, v2: <string>): <string> {
  return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomly<Number>(v1: <number>, v2: <number>): <number> {
  return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomly<URL>(v1: <URL>, v2: <URL>): <URL> {
  return Math.random() <= 0.5 ? v1 : v2;
}
chooseRandomly<String>("勝ち", "負け");
chooseRandomly<Number>(1, 2);
chooseRandomly<URL>(urlA, urlB);

String, Number, URLとそれぞれ別の型が来ていますよね。

ジェネリクスで書く

これを基にして、TypeScriptのジェネリクスの文法で書き直してみると・・・

function chooseRandomly<T>(v1: T, v2: T): T {
  return Math.random() <= 0.5 ? v1 : v2;
}
chooseRandomly<string>("勝ち", "負け");
chooseRandomly<number>(1, 2);
chooseRandomly<URL>(urlA, urlB);

ついに<T>が登場しました。
<T>は型変数名の定義です。慣習としてTがよく使われますが、AでもTypeでも構わないようです。
Tは型変数を参照しています。つまり呼び出し元の<number>などを見ているようです。
<T>を使うことにより、どんなデータ型でも扱えるようにしたいということを伝えることができます。
また、<T>は仮置きの型、実質的な中身のない型のようです。
これを使って下記のコードを実行すると、

function chooseRandomly<T>(v1: T, v2: T): T {
  return Math.random() <= 0.5 ? v1 : v2;
}
let str = chooseRandomly<string>(0, 1);
str = str.toLowerCase();

Argument of type 'number' is not assignable to parameter of type 'string'.
というコンパイラエラーが出るようです。
ありがたいですね。
受け取った型をそのまま代入しているので、渡した型そのままとなります。
また、アロー関数の場合だと

const chooseRandomly = <T,>(v1: T, v2: T): T => {
  return Math.random() <= 0.5 ? v1 : v2;
}
chooseRandomly<string>("勝ち", "負け");
chooseRandomly<number>(1, 2);

ジェネリクスであることを明示するために ,が必要になるそうです。
また、extendsを使うことで型の制約も可能のようです。

function changeBackgroundColor<T extends HTMLElement>(element: T) {
  element.style.backgroundColor = "red";
  return element;
}

上記の例だと、<T>にするとコンパイルに失敗します。
理由としては、ジェネリクスの型Tは任意の型が指定可能なので、渡す型によってはstyleプロパティが存在しない場合があるからです。
そのため、ンパイラは存在しないプロパティへの参照が発生する可能性を検知してコンパイルエラーとしているのです。
そのため、<T extends HTMLElement>と指定して、styleプロパティに安全にアクセスできるようになります。

まとめ

ジェネリクスと聞くと難しく聞こえますが、
色々な場面で使うことができそうなので、ぜひ試してみてください。
サバイバルTypeScriptのジェネリクスの項目を読むと、さらに深く理解できると思うので、
ぜひ読んでみてください。

参考
https://typescriptbook.jp/reference/generics/built-in-libraries-using-generics
https://typescriptbook.jp/reference/generics
https://typescriptbook.jp/reference/generics/type-parameter-constraint

Discussion