TypeScript勉強会第二回（2022/1/17） - ジェネリクス編 -

[第一回はこちら](https://qiita.com/KuwaK/items/b5de29fe21af3724f316)

今回の詳しい内容は以下の公式ドキュメントにまとまっているので、英語が読める人はそちらを読んだほうがいいかも

https://www.typescriptlang.org/docs/handbook/2/types-from-types.html

今回はよく使う表現だけかいつまんで説明する

## 前提知識

ジェネリクスの話をする前に、ジェネリクスとともによく使われる文法をいくつか紹介します

### `keyof`演算子

keyof演算子は、指定したObject型のメンバのキーをString LiteralのUnion Typeとして列挙する演算子

以下のような感じで動作する

```.ts
type A = {
   a: string,
   b: number
}

type a = keyof A // => 'a' | 'b'
```

### `typeof`演算子

typeof 演算子は、JavaScriptのObjectから型を作り出すことができる演算子。
この演算子があることによって、わざわざ型を別定義する必要がなくなるので非常に便利。

動作するプログラムコードの世界（JavaScriptの世界）の情報を型の世界にもっていく事ができるのでよく使われる

以下のように動作する

```.ts
const A = {
   a: 'aaa',
   b: 0
}

type a = typeof A // => { a: string, b: number }
```

### Indexed Access Types

Indexed Access Typesは、ある型の指定したメンバの型を返す演算子
例えば、`user`オブジェクトの`age`というプロパティの型を取得したいときに使える

```.ts
type Person = { 
  age: number
  name: string 
  alive: boolean
};
type Age = Person["age"] // => number
```

以上を踏まえた上でジェネリクスを見ていきます。

## 最も単純なジェネリクス例

以下のような引数をただ返すだけの関数を考えます。

```.js
function getArg(arg){
 return arg
}
```

この関数にどのような型をつけるのが適切でしょうか？
もしも以下のように引数に指定できる型が決まっているのであれば、それを指定すればいいです。

```.ts
function getArg(arg: number): number{
 return arg
}
```

しかし、もしかすると引数に指定できる型にstringとnumberの両方を指定したい。というような場合があるかもしれません

```.ts
function getArg(arg: number | string): number | string{
 return arg
}

const result = getArg('テスト') // resultはstring | number型になる
```

この型付けは間違っているわけではありませんが、以下のような問題点があります

* 引数に`string`を指定した場合でも、返り値の型が `string | number`になってしまう

この問題は以下のようにanyを取れるようにしても同じです。引数になにを指定しようが返り値の型はanyになってしまいます

```.ts
function getArg(arg: any): any{
 return arg
}

const result = getArg('テスト') // resultはany型になる
```

この関数の実装から明らかなように、この関数は引数にstringを指定すればstringを返すし、numberを返せばnumberを返します。このように**引数の型から返り値の型を絞り込みたい**ときに、ジェネリクスが活躍します。

以下がジェネリクスを使ったコードです

```.ts
function getArg<T>(arg: T): T{
 return arg
}
const result1 = getArg('テスト') // resultはstring型になる
const result2 = getArg(0) // resultはnumber型になる
```

この関数内で利用できる型変数`T`というのを定義することができます。この定義は`関数名<型変数名>(){}`のような形で、関数名と引数の`()`の間に`<>`で囲んで定義します

```.ts
function getArg<T> // ⇐ここで型変数を定義
```

ここで定義した型変数Tは関数内で型付けする際に通常の型と同じように使用できます。ここでは引数argの型と返り値の型にともにTをつけました。

型変数Tの実際の型が決定するのは関数が呼び出されたときです。
TypeScriptのコンパイラは引数argに指定された型を見てそれをT型に割当て、返り値の型を推論します。これによって、**引数と同じ型を返す関数**を作ることができます。

### get関数の型付け
以下のような関数 `getValue`に型をつけることを考えます。

```.js

const a = {
 name: 'a',
 age: 10
}

function getValue(key){
  return a[key]
}
```

この関数は、以下の性質を持っています。
* 引数keyはaのプロパティのみ指定できる
* 返り値の値は引数に指定したkeyのvalueになる

この性質を型を使って表すことを考えます。

まず、引数keyは、aのプロパティのみを指定できるようにしたいので、`keyof`演算子と`typeof`演算子を使って以下のように表すことができます。

```.ts
function getValue(key: keyof typeof a){
  return a[key]
}
```

また、返り値はaのvalueの値になるので、以下のように表すことができます。

```.ts
function getValue(key: keyof typeof a): (typeof a)[keyof typeof a]{
  return a[key]
}
```

しかしこのままでは引数に指定したkeyのvalueを返り値の型に指定することができていません。

```.ts
const a = {
   name: 'a',
   age: 10
}

function getValue(key: keyof typeof a): (typeof a)[keyof typeof a]{
  return a[key]
}

const aa = getValue('name') // => string | numberと推論される
const bb = getValue('age') // => string | numberと推論される
```

しかしジェネリックを使えば引数に応じて返り値の型を変えることができます。
まず、以下のように引数の型を型変数Tとし、extends演算子を使ってTの型を縛ります。
ここでの `T extends keyof typeof a`の意味は、T型は `keyof typeof a`型と互換性のある型に制限するという意味です。

```.ts
function getValue<T extends keyof typeof a>(key: T): (typeof a)[keyof typeof a]{
  return a[key]
}
```

そしてここで指定した型変数Tを使って返り値の型を書き直します。
こうすることで、型変数Tを経由して引数の型と返り値の型を連動させることができます。

```.ts
const a = {
   name: 'aaa',
   age: 0
}

function getValue<T extends keyof typeof a>(key: T): (typeof a)[T]{
  return a[key]
}

const aa = getValue('name') // => stringと推論される
const bb = getValue('age') // => numberと推論される
const cc = getValue('ccc') // => 存在しないkeyなのでエラーになる
```

### 練習問題

以下のようなset関数に型付けをしてみましょう

```.ts
const a = {
   name: 'aaa',
   age: 0
}

function setValue(key, value){
  a[key] = value
}
```

<details><summary>答え</summary><div>

```.ts
const a = {
   name: 'aaa',
   age: 0
}

function setValue<T extends keyof typeof a>(key: T,value:(typeof a)[T]){
   a[key] = value
}
```
</div></details>