【TypeScript】is keyof を使用してオブジェクトのプロパティを持っているか判定する

## code
```typescript
interface Post {
  id: number
  content: string
  createdAt: Date
}

const post: Post = {
  id: 1,
  content: 'textContent',
  createdAt: new Date(),
}

const keys = Object.keys(post)
const checkKey: string = 'id'

if (keys.includes(checkKey)) {
  console.log(post[checkKey])  // エラー①
}

if (['id', 'content', 'createdAt'].includes(checkKey)) {
  console.log(post[checkKey])  // エラー②
}

if ('id' === checkKey) {
  console.log(post[checkKey])  // 成功③
}

const checkPostType = (
  attribute: string,
  keys: string[]
): attribute is keyof Post => {
  return keys.includes(attribute)
}

if (checkPostType(checkKey, keys)) {
  console.log(post[checkKey])  // 成功④
}

```

## 初めに
TypeScriptを書いていると特定のオブジェクトに対して動的なKeyで値を取得したいケースがありました
改めて型ガードについて調べた時に良い感じに書けたので載せます

また、上記に載せているコードを元に話を進めていきます

## コードについて
ここではアクセスするオブジェクトを`post`として設定し、型として`Post`を設定します。
動的なkeyの役割として`checkKey`に対して`post`の属性である`id`を明示的に設定してますが、実際には何が入るかわからないため、型はstringにしています

後はこれでできそうな方法を4つ載せてそれぞれについて見ていきましょう

### エラー① `keys.includes(checkKey)`
```typescript
const keys = Object.keys(post)
const checkKey: string = 'id'

if (keys.includes(checkKey)) {
  console.log(post[checkKey])  // エラー①
}
```
`keys`には対象の`post`のプロパティ名一覧が入り、それに`checkKey`が含まれているかをチェックするので
最初にこれが思い浮かびましたが残念ながらエラーとなります。


### エラー② `['id', 'content', 'createdAt'].includes(checkKey)`
```typescript
const keys = Object.keys(post)
const checkKey: string = 'id'

if (['id', 'content', 'createdAt'].includes(checkKey)) {
  console.log(post[checkKey])  // エラー②
}
```
次は`keys`の箇所を明示的に指定しました。
見るに堪えないコードかもしれませんが一度試してみましたが、ダメでした。

エラー①も同様ですが、ここでチェックしても条件分岐の内部で`checkKey`の型が`string`として判定されているのが原因です

### 成功③ `'id' === checkKey`
```typescript
const keys = Object.keys(post)
const checkKey: string = 'id'

if ('id' === checkKey) {
  console.log(post[checkKey])  // 成功③
}
```
次は`keys`の箇所を明示的に指定しました。
先ほどまでは`checkKey`がstring型として判定されましたが、明確に`id`と比較することで
`checkKey: id`というリテラル型として認識され`post`のプロパティに存在する`id`と同一であることが保証されて成功したようです。

ただ、この書き方だと「textContentやcreatedAtなど他のプロパティを取るとき」や「Post型のメンバーが増えた時」に変更が加わるので好ましくはないですね

```typescript
// 好ましくない例
if ('id' === checkKey || 'textContent' === checkKey || 'createdAt' === checkKey) {
  console.log(post[checkKey])
}
```

### 成功④ `~  is keyof Post`

最後ですが、`is`と`keyof`を使用してcheckKeyをPostのプロパティのユニオン型として扱われるように変換してます
それぞれ`is`と`keyof`を単体で見ていきます

#### isについて
```typescript
const isPost = (obj: any): obj is Post => {
  return obj.textContent !== undefined && obj.createdAt !== undefined
}

const notPost = 'not post'

if(isPost(notPost)){
  console.log(notPost.id + 1)
}else{
  console.log('not Post type')
}
```
ユーザー定義した型かどうかを判定するために使えます

関数内で真偽値を返した時に、真であるなら引数として指定したプロパティを特定の型として扱うことができます
上記の例では`isPost`を作成し、引数で受け取ったobjのプロパティとして`textContent`,`createdAt`が存在すれば
objはPost型として扱うという内容になります。

ただし、真であるならその型として扱われるため厳密な判定をしっかりしないと全然エラーになります。
(下記が一例)

```typescript
const isPost = (obj: any): obj is Post => {
  return true
}

const c = 'not post'

if(isPost(notPost)){
  console.log(notPost.id + 1) // 存在しないプロパティにアクセスされ、実行時エラーとなる
}
```

#### keyofについて
```typescript
interface Tweet{
  userId: number
  content: string
}

type KeyofTweetKeys = keyof Tweet
type UnionTweetKeys = 'userId' | 'content'
```

`UnionTweetKeys`と`KeyofTweetKeys`は同じ内容を表します

型として定義したプロパティをユニオン型として返します

#### 成功④解説
```typescript
const keys = Object.keys(post)
const checkKey: string = 'id'

const checkPostType = (
  attribute: string,
  keys: string[]
): attribute is keyof Post => {
  return keys.includes(attribute)
}

if (checkPostType(checkKey, keys)) {
  console.log(post[checkKey])  // 成功④
}
```

先ほど説明した`is`,`keyof`を踏まえてみていきます。
今回は新しく`checkPostType`を定義し、`attribute is`を設定しているので
真を返す場合`attribute`は、`keyof Post`として扱います。

そのため、引数として受け取ったattributeがkeysに含まれている場合Postのプロパティのユニオン型として扱うため
条件分岐内でプロパティにアクセスできます


#### ジェネリクスを使用する例
```typescript
interface Tweet {
  userId: number
  content: string
}

const checkType = <T extends string>(
  attribute: string,
  keys: string[]
): attribute is T => {
  return keys.includes(attribute)
}

const tweet: Tweet = {
  userId: 123,
  content: 'hoge',
}
const tweetKeys = Object.keys(tweet)
const checkTweetKey: string = 'id'

if (checkType<keyof Tweet>(checkTweetKey, tweetKeys)) {
  console.log(tweet[checkTweetKey])
}
```

自分はジェネリクスを使う機会が多くはないのですが
今回作成したものだと特定の型に限定されてしまいますが上手く型指定をして汎用的にした一例です

判定が真の時、第一引数に渡した値をジェネリクスとして指定した`keyof Tweet`として扱います

また、`const checkType = <T extends string>`としているのは
`keyof`として受け取る値は文字列のユニオン型のためです


## 参考リンク
https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard
https://future-architect.github.io/typescript-guide/generics.html#id4