TypeScriptのジェネリック解析

## ジェネリック入門

ジェネリックは、**型を変数として型定義に渡す**ということを簡単に言えば、関数に引数を渡すのと同じようなものです。例えば：

```typescript
function identity<Type>(arg: Type): Type {
  return arg
}

let output = identity<string>('myString')
```

`<>`で`Type`を囲むことで、ジェネリック関数に型を渡すことができます。上記の関数は、`Type`型の引数を受け取り、`Type`型の結果を返します。

`Type`は引数なので、名前も自由ですが、一般的には`T`という名前を使用することがよくあります。したがって、以下のように書くこともできます：

```typescript
function identity<T>(arg: T): T {
  return arg
}
```

### 型の推論

ジェネリック関数を使用する際には、`T`の型を明示的に指定する必要はありません（実際、通常はそうします）。この場合、TS は`T`の型を自動的に推論します：

```typescript
let output = identity('myString')
// outputもstring型です
```

先ほどの例でも、型`<string>`を明示的に指定しない場合、TS は`"myString"`の型を`T`として推論するため、関数の戻り値も文字列になります。

### 型の制約

デフォルトでは、ジェネリックは任意の型になりますが、これでは可読性が低くなりますし、ジェネリックを適用した型に対して操作やメソッドの呼び出しを行う際には、任意の型であるため必ずチェックに通過しません。この問題を解決するために、`extends`を使用してジェネリックに制約を設けることができます。

```typescript
interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length) // argは必ずlengthプロパティを持つため、型チェックを通過します
  return arg
}
```

上記のコードでは、`<T extends Lengthwise>`によって`T`が`length`プロパティを持つ型であることを示しています。`length`プロパティを持つ任意の型は、このジェネリックの要件を満たします。例えば、配列でも可能です。

## バインディング能力

公式ウェブサイトによると、ジェネリックは型の再利用に使用されると述べていますが、上記の簡単な説明を見ると、これは非常に効果的であることがわかるでしょう。しかし、ジェネリックは型の再利用だけでなく、他の用途もあります。

私の答えは、型の連動です。**T は同じ型定義内で使用される他のジェネリックにバインドすることができます**。

再度、この例を見てみましょう。実際には、入力の型と出力の型をバインドしています：

```typescript
function identity<Type>(arg: Type): Type {
  return arg
}
```

以下は、より明確な「型のバインディング」の例です。

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

let x = { a: 1, b: 2, c: 3, d: 4 }

getProperty(x, 'a') // 可能
getProperty(x, 'm') // エラー、`Key`は`Type`のキーにバインドされるため、`m`は`Type`のキーではありません
```

### マッピング型

```typescript
const myMap = {
  a: () => {},
  b: (someString: string) => {},
  c: (someNumber: number) => {},
}
type MyMap = typeof myMap
type MyKey = keyof MyMap
```

キーが`a`、`b`、`c`であり、値が異なる関数であるオブジェクトがあるとします。このオブジェクトのキーと対応する関数の引数のオブジェクトの型を取得する必要がある場合、どのように実現できるでしょうか？

```typescript
const myMap = {
  a: () => {},
  b: (someString: string) => {},
  c: (someNumber: number) => {},
}

function wrapper<K extends keyof typeof myMap>(key: K, fn: (typeof myMap)[K]) {
  return async function (...arg: Parameters<typeof fn>) {
    // do something
    await Promise.resolve()
    fn.apply(null, arg)
  }
}

const wrappedMap: {
  [K in keyof typeof myMap]: ReturnType<typeof wrapper>
} = {} as any

for (const key in myMap) {
  const k = key as keyof typeof myMap
  wrappedMap[k] = wrapper(k, myMap[k])
}
```

この回答では、ジェネリック `K` を使用して `key` のタイプを制限し、それが `myMap` のキーであることを確認しています。その後、`typeof myMap[K]` を使用して、`myMap` 中の対応するキーの関数タイプを取得しています。`wrapper` 関数では、`Parameters<typeof fn>` を使用して関数 `fn` のパラメータータイプを取得しています。最後に、マップ型 `[K in keyof typeof myMap]: ReturnType<typeof wrapper>` を使用して、`wrappedMap` のタイプを定義し、そのキーと値が `myMap` と対応していることを確認しています。TypeScript は `wrappedMap` のタイプを推測できないため、型チェックを回避するために `as any` を使用しています。

```typescript
const myMap = {
  a: () => {},
  b: (someString: string) => {},
  c: (someNumber: number) => {},
}
type MyMap = typeof myMap
type MyKey = keyof MyMap

function wrapper<K extends MyKey, T extends MyMap[K]>(_key: K, fn: T) {
  return async function (...arg: Parameters<T>) {
    await Promise.resolve()
    ;(fn as any).apply(null, arg)
  }
}

type WrappedMap = {
  [K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>
}

const wrappedMap: Partial<WrappedMap> = {}
for (const key in myMap) {
  const k = key as MyKey
  wrappedMap[k] = wrapper(k, myMap[k])
}
```

現在、`WrappedMap` の型はラッパーの戻り値の効果を持っていることは確かですが、`(fn as any).apply(null, arg)` という部分は突然現れたように見えますね。

なぜ `fn` を `any` にする必要があるのでしょうか？

なぜなら、TS にとって `a`、`b`、`c` は値の引数の型と結びついていないからです。したがって、`T` を使用して制約をかけても効果がありません。少しわかりにくいかもしれませんが、次の回答 2 を見ればより明確になるかもしれません。

### 回答 2

```typescript
const myMap: MyMap = {
  a: () => {},
  b: (someString: string) => {},
  c: (someNumber: number) => {},
}
interface MyMapArgs {
  a: []
  b: [someString: string]
  c: [someNumber: number]
}

type MyMap = {
  [K in keyof MyMapArgs]: (...args: MyMapArgs[K]) => void
}

type MyKey = keyof MyMap

function wrapper<K extends MyKey, F extends MyMap[K]>(_key: K, fn: F) {
  return async function (...arg: Parameters<F>) {
    await Promise.resolve()
    fn.apply(null, arg)
  }
}

type WrappedMay = {
  [K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>
}

const wrappedMap: Partial<WrappedMay> = {}
for (const key in myMap) {
  const k = key as MyKey
  wrappedMap[k] = wrapper(k, myMap[k])
}
```

`(fn as any)` を削除する解決策は、まず関連付けたい要素を別のマップにマッピングし、上記の `MyMapArgs` のようにします。そして、このマッピングを使用して `MyMap` を作成します。これにより、TS はついにこれらの 2 つの要素が関連していることを理解します。

P.S. より詳細な情報については、[issues#30581](https://github.com/microsoft/TypeScript/issues/30581) と [pull#47109](https://github.com/microsoft/TypeScript/pull/47109) を参照してください。

---------

この記事は [ssshooter.com](https://ssshooter.com/ja/typescript-generics/) で初めて公開されました。