TypeScriptのジェネリクスを覚えた

## これは何

日常的にTypeScript/Reactでコードを書いているものの、主にUIの作り込みが多いのでTypeScriptの知識としてはそこまで深く知らなくても仕事がこなせてました。
そこで、ちゃんとTypeScriptを書けるようになりたいと思い改めて知らなかったことを書いた記事になります。


## ジェネリクスを知ったきっかけ
[type-challenges](https://github.com/type-challenges/type-challenges)が流行っているので、試しに挑むも1問も解けず、改めて[サバイバルTypeScript](https://typescriptbook.jp/)から勉強し直したのが、ジェネリクス型を知ったきっかけでもあります。

## ジェネリクスとはどういう意味？

> ジェネリクス（generics）とは、プログラミング言語の機能・仕様の一つで、同じコードで様々な異なるデータ型のデータを処理できるようにする仕組み。C++言語などでは、ほぼ同様の機能を「テンプレート」(template)という。

[IT用語辞典](https://e-words.jp/w/%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%82%AF%E3%82%B9.html)より

プログラミングにおいて、特定のデータ型に縛られることなく、さまざまなデータ型に対応できる柔軟なプログラムを作成することが出来るようになるようです。

## 実際の例

```typescript
function identity(arg: any): any {
  return arg;
}
```
上記のコードは[公式](https://www.typescriptlang.org/docs/handbook/2/generics.html)のコードからコピペしてきたものです。
引数の型が`any`になっています。
これだと、あらゆる型を受け入れるという意味では汎用的ではあるけど、そもそもの型情報が何だったのかが分からなくなります。
そしてコンパイル時に型のチェックが行われないため、予期しない型が渡された場合にエラーが発生する可能性があります。

公式のコードを少し変更してみます。

```typescript

const identity(arg: any): any {
  console.log(arg.toUpperCase()); //stringを想定している
}

console.log(identity("hello"));
console.log(identity(1)); //実行されるまでエラーは出ない

```

すると以下の様な実行結果が出てきます。

```bash
HELLO
error: Uncaught (in promise) TypeError: arg.toUpperCase is not a function
```

この場合、`arg`が文字列であることを仮定して`.toUpperCase()`を呼び出していますが、数値を渡すとエラーになります。ジェネリクスを使えば、このような問題を防げます。
ジェネリクスを使用したコードが以下です。
```typescript
function identity<Type>(arg: Type): Type {
  return arg;
}

const hello = identity<string>("hello").toUpperCase();
const one = identity<number>(1);

console.log(hello);
console.log(one);

```



実行結果が以下になります。
```bash
HELLO
1
```

型引数を省略することもできるようです。

```typescript

const hello = identity("hello").toUpperCase(); // string
const one = identity(1); // number
```

## ジェネリクスの型変数

さきほどのコードで書いた関数`identity`の後に記述した`<Type>`というのが型変数になります。
ここで名前を`Type`と付けていますが型「変数」なので、ある程度自由に名前を付けることができます。
ただ慣習みたいなものがあり、大文字の`T`で書かれることが多いようです。

```typescript
function identity<T>(arg: T): T { //型変数をTに変更
  return arg;
}
```
> TypeScriptの慣習として、型変数名にはTを用いることが多いです。このTはtemplateの略と言われています。
単純なジェネリクスで、型変数が2つある場合は、TとUが用いられることがあり、その理由はアルファベット順でTの次がUだからです。この規則にしたがって、3つ目の型変数はVとする場合もあります。

[型変数の慣習](https://typescriptbook.jp/reference/generics/type-variables#%E5%9E%8B%E5%A4%89%E6%95%B0%E5%90%8D%E3%81%AE%E6%85%A3%E7%BF%92)より


## 型引数に制約をつける

ジェネリクスの型引数を特定の型に限定することができます。

```typescript
function identity<T extends string | boolean>(arg: T): T {
	return arg;
}
```
型変数`T`の後に`extends`と記述してジェネリクスの型Tを特定の型に限定することができます。
上記ではstring型とboolean型に限定しています。

では下記のコードはどうなったのでしょうか？

```typescript
const hello = identity<string>("hello").toUpperCase();
const one = identity<number>(1);
```

エディタ上でちゃんとエラーが出ていることが分かります。

![スクリーンショット 2024-12-23 0.03.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3443/84b5b6ee-3228-b7d5-7228-6cbe8f0841c0.png)

`extends`以外でも`implements`というキーワードがあり、`implements`はインターフェースでの限定に使われるようです。

```typescript
interface ValueObject<T> {
  value: T;

  toString(): string;
}

class UserID implements ValueObject<number> {
  public value: number;

  public constructor(value: number) {
    this.value = value;
  }

  public toString(): string {
    return `${this.value}`;
  }
}

class Entity<ID extends ValueObject<unknown>> {
  private id: ID;

  public constructor(id: ID) {
    this.id = id;
  }

  //...
}
```
> EntityクラスはValueObjectインターフェースを実装しているクラスをIDとして受ける構造になっていますが19行目にあるようにこのときの型引数の制約はimplementsではなくextendsでなければなりません。

[型引数に制約をつける](https://typescriptbook.jp/reference/generics/type-parameter-constraint#%E5%9E%8B%E5%BC%95%E6%95%B0%E3%81%AB%E5%88%B6%E7%B4%84%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B)より


あとextendsは一つしか継承できないけど、implementsは複数継承できるという違いがあるようです。


## 参考

https://www.typescriptlang.org/docs/handbook/2/generics.html

https://typescriptbook.jp/reference/generics