[TypeScript] ジェネリックをマスターしよう！

### 概要
TypeScriptのジェネリックについてまとめました。

### 【目次】
[まずは基本的な型宣言をマスターしよう！](https://qiita.com/PenPe/items/c189a611b3407b7ceef9)
[オブジェクト型の型宣言をマスターしよう！](https://qiita.com/PenPe/items/440683cc6e5e4ac0ca32)
[関数の型定義をマスターしよう！](https://qiita.com/PenPe/items/bff109c6f83f46d191a7)
[配列をマスターしよう！](https://qiita.com/PenPe/items/7e056eec2cce76a745a9)
[インターフェースをマスターしよう！](https://qiita.com/PenPe/items/f9314aeccb2f23cac382)
[クラスを使いこなそう！](https://qiita.com/PenPe/items/c8e6289f386c4db23a6f)
[型修飾子についての理解を深めよう！](https://qiita.com/PenPe/items/7af57861e93830fe9a79)
[ジェネリックをマスターしよう！](https://qiita.com/PenPe/items/fde6a342e746e265d907) ←🈁
[独自の構文拡張をマスターしよう！](https://qiita.com/PenPe/items/c79cdb5bc4a68a1dfd7f)

--

### ジェネリックとは？
ジェネリックとは **実際に利用されるまで型が確定しない抽象的なデータ型** です。
以下のように宣言します。
- `<T>`

---

### ジェネリックの命名規則
一般的な命名規則についてご紹介します。

|   命名   |     利用ケース                                  | 
| ---- | ------------------------------------- | 
| T    | 標準的な命名<br>`type`, `template` を表す | 
| U, V | 複数パラメータがある場合に使用する              | 
| S    | 状態管理のライブラリ<br>`state`を表す         | 
| K, V | データ構造を利用する場合に用いられる<br>`K` が`key`, `V`が`value`を指す | 

**[注意点]**
コンテキストでの意図が伝わりにくい場合は、**より具体的な命名をする**こと。

これらを踏まえて実際の実装を見ていきましょう。

---

### ジェネリック関数
ジェネリック関数は以下のように定義します。
```typescript
function genericFunc<T>(input: T) {
    return input;
}

let typeString = genericFunc("string");
let typeNumber = genericFunc(1234);
```

ジェネリック関数を使用することで任意の型を引数に取る関数を作成できます。

#### :memo: [注意点]アロー関数
アロー関数を使用すると、`JSX`の構文と競合するのでエラーが発生します。
```typescript
// NG
let genericAllow<T> = (input: T) => input;
```

---

### 明示的な型指定
ジェネリックは明示的に型（パラメータ）を指定することが出来ます。

ジェネリックインターフェースの利用、ジェネリッククラス継承時に利用します。
```typescript
function genericFunc<T>(input: T) {
    return input;
}

let typeString = genericFunc<string>("string");
let typeNumber = genericFunc<number>(1234);
```

---

### ジェネリックインターフェース
ジェネリックはインターフェースに対しても指定することが可能です。
インターフェース利用時に明示的に型指定する必要があります。

```typescript
interface genericInterface<T> {
    prop: T
}

let hoge: genericInterface<string> = {
    prop: "abc"
}
```

#### :memo: [参考] Arrayメソッド
Arrayメソッドはジェネリックインターフェースとして定義されています。
```typescript
interface Array<T> {
    pop(): T | undefined;

    push(...items: T[]): number
}
```

---

### ジェネリッククラス
クラスに対しても、ジェネリックを指定することが可能です。

```typescript
class GenericKlass<T> {
    prop: T

    constructor(prop: T){
        this.prop = prop;
    }
}
```

#### クラスの拡張
ジェネリッククラスをベースクラスとしたクラスの拡張も可能です。
デフォルトの型を持たないパラメータに対しては明示的な型指定が必要です。

```typescript
class GenericKlass<T> {
    prop: T

    constructor(prop: T){
        this.prop = prop;
    }
}

class SubKlass extends GenericKlass<string> {}
```

サブクラスもジェネリッククラスとし、型パラメータをベースクラスに対しても指定することも可能です。
```typescript
class GenericKlass<T> {
    prop: T

    constructor(prop: T) {
        this.prop = prop;
    }
}

class SubKlass<T, U> extends GenericKlass<T> {
    types: U

    constructor(prop: T, types: U) {
        super(prop);
        this.types = types
    }
}
```

#### ジェネリックインターフェースの実装
ジェネリックインターフェースの実装は、以下のように行います。

```typescript
interface SampleInterface<T> {
    value: T
}

class Klass<T> implements SampleInterface<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }
}
```

#### ジェネリックメソッド
クラスメソッドは独自のジェネリック型を定義することが可能です。
クラスメソッドの呼び出しでは、呼び出しごとに異なるパラメータを指定することが出来ます。

```typescript
class Klass<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    genericMethod<U>(input: U) {
        return {value: this.value, key: input}
    }
}

let klass = new Klass("abc");
klass.genericMethod(1234);
klass.genericMethod("def");
```

#### 静的メンバーとジェネリック
静的メンバーもジェネリックを利用できますが、
クラスに紐づいているため、インスタンス固有のパラメータのアクセスはできません。

アクセスした場合、以下のようにエラーが発生します。

```typescript
class Klass<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    static staticMethod<V>(input: V) {
        //NG: Static members cannot reference class type parameters.
        let tmp: T;
    }
}
```

---

### ジェネリック型エイリアス
型エイリアスもジェネリックを利用することが出来ます。

```typescript
type SampleAlias<Input, Output> = (input: Input) => Output
let inputStringOutputNumber: SampleAlias<string, number>;

// OK
inputStringOutputNumber = (input: string) => input.length;

// NG: Type '(input: string) => () => string' is not assignable to type 'SampleAlias<string, number>'.
inputStringOutputNumber = (input: string) => input.toUpperCase();
```

#### :memo: [デザインパターン] ジェネリック + タグ付き合併型
ジェネリック型とタグ付き合併型を組み合わせることで、再利用可能な型をモデル化することが出来ます。

サンプルコードでは、型が異なる`data`を格納するログデータをモデル化しています。
これにより、処理を再利用することができます。

```typescript
interface SuucessLog<T> {
    data: T,
    type: "success"
}

interface FailreLog<T> {
    data: T,
    type: "fail"    
}

type Log<T> =  SuucessLog<T> | FailreLog<T>

function checkLog<T>(log: Log<T>) {
    if(log.type === "success") {
        console.log("successed!");
        console.log(`data is ${log.data}`);
    } else {
        console.log("failed!");
        console.log(`data is ${log.data}`);
    }
}

let logFirst: Log<string> = {
    data: "process1 OK",
    type: "success"
}

let logSecond: Log<string> = {
    data: "process2 OK",
    type: "success"
}

let logThird: Log<number> = {
    data: 19389328103941,
    type: "fail"
}

checkLog(logFirst);
checkLog(logSecond);
checkLog(logThird);
```

---

### ジェネリックのデフォルト値
型パラメータにはデフォルトの型を指定することが可能です。
`<T = デフォルト型>`

ジェネリックインターフェースの利用, クラス継承時の明示的な型指定を、デフォルト値を用いることで省略できます。
```typescript
interface genericInterface<T = string> {
    prop: T
}

let hoge: genericInterface = {
    prop: "abc"
}
```

---

### 制限付きジェネリック
ジェネリック型にはどんな値も渡すことが可能です。
しかし、特定のセットの方だけを扱うことを意図したい場合もあるでしょう。
<br>

`extends`キーワードを指定することで、型パラメータがある型を拡張する必要があると宣言することができます。

以下のサンプルコードでは、
`CallLength`関数に渡される引数が`WithLength`に割当可能であることを制限しています。

```typescript
interface WithLength {
    length: number
}

function CallLength<T extends WithLength>(input: T){
    input.length
}

CallLength("call CallLength");
CallLength([1234, 4567]);

// NG: Argument of type 'Date' is not assignable to parameter of type 'WithLength'.
CallLength(new Date());
```

---

`extends WithLength`の指定がない場合を見ていきましょう。

以下のエラーが発生します。
渡された方パラメータが`legnth`を保持していない可能性を示唆していますね。

```typescript
interface WithLength {
    length: number
}

function CallLength<T>(input: T){
    // NG: Property 'length' does not exist on type 'T'.
    input.length
}
```

#### :memo: [応用] 制限付きジェネリック + keyof 演算子
制限付きジェネリックと、`keyof`演算子を連携することで、よい開発者体験を得ることが出来ます。

サンプルコードでは`Key extends keyof T`を利用して、関数`get`の引数を制限しています。
`T[Key]`が返るので、戻り値の型は`string` or `number` となります。

```typescript
function get<T, Key extends keyof T>(obj: T, key: Key) {
    return obj[key]
}

interface Person{ 
    name: string,
    age: number
}

let taro: Person = {
    name: "taro",
    age: 30
}

// let taroName: string
let taroName = get(taro, "name");
// let taroAge: number
let taroAge = get(taro, "age");
```

**[参考]**
`keyof` 演算子を利用した場合は以下の挙動になります。

`T[keyof T]`のデータが返るので、戻り値がより抽象的な`string | number`型となります。

```typescript
function get<T>(obj: T, key: keyof T) {
    return obj[key]
}

interface Person{ 
    name: string,
    age: number
}

let taro: Person = {
    name: "taro",
    age: 30
}

// let taroName: string | number
let taroName = get(taro, "name");
// let taroAge: string | number
let taroAge = get(taro, "age");
```

---

### :memo: [参考] Promise
TypeScriptでは、`Promise`はジェネリッククラスとして宣言されています。
このパラメータには、`Promise`が成功された(`fulfilled`)ときに返す値の型を指定します。

#### 明示的な型指定
型指定した場合のサンプルコードはこちらです。
指定された型と、`resolve`の引数型が一致しない場合は型エラーが発生します。

```typescript
const unknownPromise = new Promise((resolve) => {
    setTimeout(() => {
        console.log("1seconds");
        resolve("done");
    }, 1000);
});

const stringPromise = new Promise<string>((resolve) => {
    setTimeout(() => {
        console.log("1seconds");
        resolve("done");
    }, 1000);
});

const numberPromise = new Promise<number>((resolve) => {
    setTimeout(() => {
        console.log("1seconds");
        resolve(1234);
    }, 1000);
});
```

#### 型指定がない場合
明示的に型を指定しない場合、パラメータの型が `unknown`であることを推論します。
以下のサンプルコードでは、型指定がないことが原因でエラーが発生しています。

```typescript
function resolveString() {
    return new Promise<string>((resolve) => setTimeout(()=> resolve("done"), 1000))
}     

function resolveNumber() {
    return new Promise((resolve) => setTimeout(()=> resolve(12.34), 1000))
}

// NG: 'result' is of type 'unknown'.
function asyncProcess() {
    resolveString().then(resolveNumber).then(result => console.log(result.toFixed()));
}
```

#### :memo: [参考] Promiseの実践コード
```typescript
import fs from 'fs/promises';

/**
 * ファイルを読込
 * @param {string} filePath - 読み込むファイルのパス
 * @returns {Promise<string>} - 読み込まれたファイルの内容を表すPromise
 */
function readFile(filePath: string): Promise<string> {
    return fs.readFile(filePath, 'utf8');
}

/**
 * データ加工
 * @param {string} data - 加工するデータ
 * @returns {Promise<string>} - 加工されたデータを表すPromise
 */
function processData(data: string): Promise<string> {
    return new Promise((resolve) => {
        const processedData = data.toUpperCase();
        resolve(processedData);
    });
}

/**
 * APIリクエスト
 * @param {string} data - 送信するデータ
 * @returns {Promise<Response>} - APIからのレスポンスを表すPromise
 */
function sendData(data: string): Promise<Response> {
    return fetch('https://example.com/api/data', {
        method: 'POST',
        body: JSON.stringify({ data }),
        headers: { 'Content-Type': 'application/json' }
    });
}

// 実行
readFile('path/to/file.txt')
    .then(processData)
    .then(sendData)
    .then(response => response.json())
    .then(json => console.log('Response from API:', json))
    .catch(error => console.error('Error:', error));
```

---

### まとめ
以上です！
ジェネリックを利用するのは`C#`以来でしたので、楽しみながら学習できました。