【TypeScript】ジェネリクス、lookup型、keyof型、部分型がまとめて理解できるコード例

# 目標

以下のコードが理解できるようになることが本記事の目標です！

```tsx
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

type Human = {
	name: string;
	age: number;
}

const tanaka: Human = {
  name: "tanaka",
  age: 26
}

const tanakaName = get(tanaka, "name"); // tanakaNameはstring型
console.log(tanakaName); // 出力： "tanaka"

const tanakaAge = get(tanaka, "age"); // tanakaAgeはnumber型
console.log(tanakaAge); // 出力： 26
```

# 動作解説

定義されている`get`関数がやっていることは、引数として渡された`obj[key]`を返すだけです。

`tanaka`オブジェクトの`name`プロパティにアクセスすれば、`”tanaka”`が取得できますし、`age`プロパティにアクセスすれば`26`が取得できます。

型を省いてJavaScriptで実装すると以下のようになります。とてもシンプルですね。

```jsx
function get(obj, key) {
  return obj[key];
}

const tanaka = {
  name: "tanaka",
  age: 26
}

const tanakaName = get(tanaka, "name");
const tanakaAge = get(tanaka, "age");
```

ただ、これをTypeScriptで実装しようとする時、いくつか問題点があります。

1. `get`関数の引数に決まった型がない（第１引数のオブジェクトは`Human`型であるとは限らない）
2. 返り値の型が`string`と`number`で異なる
3. `get`関数の第２引数は`tanaka`のプロパティだけを指定できるようにしたい

# ジェネリクス

ジェネリクスを使うと、型は関数を呼び出した時に決める、ということはできるようになります。これを使えば１つ目の問題が解決しますね。

簡単な例を見てみましょう。

```tsx
function test<T>(arg: T): T {
	return arg;
}

test<number>(1); // number型の値、１が返ってくる
test<string>("文字列"); // string型の値、"文字列"が返ってくる
```

抽象的な型引数`T`を関数に与え、実際に利用されるまで型が確定しない関数を作成することによって、後で型を決められるようにしています。抽象的な型引数は複数渡すことができます。

ここで最初のコードを見てみましょう。

```tsx
// 一部簡略化
function get<T, K>(obj: T, key: K): T[K] {
  return obj[key];
}
```

`get`関数には`T`と`K`という抽象的な型引数が渡されています。`T`と`K`は`get`関数呼び出し時に与えることにしている、ということです。`obj`の型は後から渡される`T`が、`key`には`K`が型として渡されます。

ですが実際に`get`関数を呼び出している部分を見てみると、`T`や`K`に該当する方が渡されていません。このような時は型推論によって`T`と`K`は決められます。`get(tanaka, "name");`とした場合、実際には以下のような型に推論されます。

```tsx
function get<Human, "name">(obj: Human, key: "name"): string
```

`T`は`Human`型、`K`は`”name”`というリテラル型に推論されます。返り値である`T[K]`は`string`に推論されていますが、これを理解するにはlookup型を理解する必要があります。

# lookup型

lookup型は`T[K]`という構文を持つ型で、`T`と`K`は型です。`T`にはオブジェクト型、`K`には文字列のリテラル型が用いられることが多いです。簡潔に書くと「`T[K]`は`T`が持つ`K`プロパティの型」ということです。

今回解読しようとしているコードでは、`T`は`Human`というオブジェクト型、`K`は`”name”`という文字列のリテラル型が割り当てられています。`Human`型を見てみると、`name`プロパティの型は`string`になっています。よって`T[K]`は`string`に推論されているというわけです。

```tsx
type Human = {
	name: string; // Human[name]のとき、T[K]はstring
	age: number; // Human[age]のとき、T[K]はnumber
}
```

lookup型を使うことによって、`get`関数の返り値の型が異なるという、２つ目の問題が解決されます。

# keyof型

最後に3つ目の問題についてですが、これは型引数`K`にくっついている`K extends keyof T`という部分が解決してくれます。この構文は`extends`と`keyof`という２種類の構文が混じっているため、一つずつ見ていきましょう。

まずは`keyof`についてです。`keyof`型は、オブジェクト型からそのオブジェクトのプロパティ名の型を得る機能です。

```tsx
type Test = {
	id: number;
	pass: string;
}
// "id" | "pass" という型と同義
type TestKeys = keyof Test;

let key: TestKeys = "id";
key = "pass";
key = "hoge" // これはエラー
```

`Testkeys`型は、`Test`型のプロパティ名だけが許されている型です。`”id”`と`”pass”`というリテラル型しか受け付けません。keyof型を使うとこのような型を簡単に作ることができます。

今回解読しようとしているコードでは、型引数`T`には`Human`型が割り当てられます。つまり`keyof T`は`”name” | “age”`ということになります。

# 部分型

続いて`extends`についてです。これは部分型という制約があることを表しています。部分型は2つの型の互換性を表すものです。

```tsx
type Animal = {
	age: number;
}

type Human = {
	name: string;
	age: number;
}
```

`Animal`型と`Human`型がこのように定義されているとき、`Human`型は`Animal`型の部分型であると言えます。`Animal`を満たすオブジェクトが`Human`を満たすとは限りませんが、`Human`を満たすオブジェクトは`Animal`を必ず満たします。同じ`age`プロパティをもち、その型も同じであるからです。このような関係にある型を部分型関係だと言えます。

部分型について理解いただけたところで、`extends`について解説します。`extends`は、型引数に対して何らかの部分型であるという制約をつけたい時に使われます。

```tsx
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
```

`get`関数の場合、型引数`K`は`typeof T`の部分型でなければならないという制約が課されています。つまり`K`は`”name” | “age”`の部分型でなければならないということです。この制約があることによって、`get`関数の第２引数は`Human`型のプロパティ名のリテラル型しか受け付けないようになっています。

# おまけ：Kにextends keyof Tを指定しないと…

ジェネリクスの解説の時に`get`関数の型は以下のように推論されると書きましたが、`key`は`string`に推論されるのではないか？と思った方がいるかも知れません。

```tsx
function get<Human, "name">(obj: Human, key: "name"): string
```

型引数 `K`に対して`extends keyof T`という制約がなければ、`key`は`string`に推論されます。しかし`K`に対して`extends keyof T`を指定しない場合、「`Type ‘K’ cannot be used to index type ‘T’`」というコンパイルエラーになります。`get`関数の返り値の型`T[K]`について、不都合が生じる可能性があるからです。`T[K]`の意味からすると、`T`は何らかのオブジェクト型で、`K`はそのオブジェクト型が持つプロパティである必要があります。`extends keyof T`がないとこれが保証できなくなるため、コンパイルエラーとなるのです。

# まとめ

今回はTypeScriptに欠かせないジェネリクスや部分型にプラスして、lookup型やkeyof型についての解説も行いました。後者の2つを使いこなすには練習が必要ですね。

コード解説の部分で提示したJavaScriptのコードを、コンパイルエラーが出ないように型を付け足していくと、今回の内容が復習できるのではないかと思います。ぜひ挑戦してみてください。

本記事に誤りや誤字脱字がございましたら、コメント等でご指摘いただけるとありがたいです。

# 参考
[ジェネリクス (generics) | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/generics)

[【TypeScript】Generics(ジェネリックス)を理解する - Qiita](https://qiita.com/k-penguin-sato/items/9baa959e8919157afcd4)

[TypeScript の lookup 型をマスターしよう - コムテブログ](https://commte.net/typescript-lookup)

[keyof型演算子 | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/type-reuse/keyof-type-operator)

[部分型って"拡張型"じゃない？集合論で捉えてみる](https://zenn.dev/axoloto210/articles/advent-calender-2023-day11)

[【Typescript入門】ややこしい？部分型について初心者向けに紹介！ - プロブロ](https://yoshiki-sano.com/ts6/)