Object.entries()の型をつよつよにしたいんだ！@typescript

## 環境

```json:package.json
"typescript": "5.0.4"
```

## 困りごと

TypeScriptで教科ごとのテストの点数を扱うクラスを定義しました。

```tsx:typescript
class ScoreClass {
  constructor(
    public english: number,
    public math: number,
    public science: number,
    public history: number
  ) {}
}
```

太郎くんのテスト点数を用いてインスタンスを生成し、英語が平均の79点以上であればそれを称えるスクリプトを書きました。

```tsx:typescript
const taroScore = new ScoreClass(90, 80, 70, 60); // 英語は90点だ！

Object.entries(taroGrade).forEach(([key, value]) => {
  if (key === 'english') {
    if (value > 79) console.log('英語の点数が平均を上回ったよ！');
    else console.log('英語の点数は平均以下だね…');
  }
});

```

しかし実行結果は想定と異なります。

```bash
英語の点数は平均以下だね…
```

よく見ると、太郎くんのテスト結果を扱うべきところに太郎くんの成績を当てはめていました。

```diff_tsx:typescript
- Object.entries(taroGrade).forEach(([key, value]) => {
+ Object.entries(taroScore).forEach(([key, value]) => {
  if (key === 'english') {
    if (value > 79) console.log('英語の点数が平均を上回ったよ！');
    else console.log('英語の点数は平均以下だね…');
  }
});
```

そういえば、テスト点数とは別で成績も定義していたのでした。

```tsx:typescript
class GradeClass {
  constructor(
    public english: 'A' | 'B' | 'C' | 'D' | 'E',
    public math: 'A' | 'B' | 'C' | 'D' | 'E',
    public science: 'A' | 'B' | 'C' | 'D' | 'E',
    public history: 'A' | 'B' | 'C' | 'D' | 'E'
  ) {}
}

const taroGrade = new GradeClass('A', 'B', 'C', 'D');
```

でもエディタが（TypeScriptが）間違いを教えてくれなかったんだもん！気づけないよ！というのが今回の困りごとでした。

## 対処法

### 1.　Typeを使う

クラスではなく型を使って定義するとTypeScriptが間違いに気づいてくれます。

```tsx:typescript
type ScoreType = {
  english: number;
  math: number;
  science: number;
  history: number;
};

type GrateType = {
  english: 'A' | 'B' | 'C' | 'D' | 'E';
  math: 'A' | 'B' | 'C' | 'D' | 'E';
  science: 'A' | 'B' | 'C' | 'D' | 'E';
  history: 'A' | 'B' | 'C' | 'D' | 'E';
};

const taroScore: ScoreType = {
  english: 90,
  math: 80,
  science: 70,
  history: 60,
};

const taroGrade: GrateType = {
  english: 'A',
  math: 'B',
  science: 'C',
  history: 'D',
};

Object.entries(taroGrade).forEach(([key, value]) => {
  if (key === 'english') {
    if (value > 79) console.log('英語の点数が平均を上回ったよ！');
    else console.log('英語の点数は平均以下だね…');
  }
});
```

```bash
error TS2365: Operator '>' cannot be applied to types 'string' and 'number'.
```

### 2.　Object.entriesに型付けする（今回はこちらを選択）

とはいえ型定義ではできることに限りがあるためクラス定義は捨てがたく、今回はこちらで対応しました（人から教えてもらったものをさも自分の手柄のように紹介します）。

まずはこんな感じでObjectのentriesメソッドに型付けします。

```tsx:typescript
// オブジェクトをループする際に型チェックしてくれる
const entries = <T extends object = object>(
  obj: T
): [keyof T, T[keyof T]][] => {
  return Object.entries(obj) as [keyof T, T[keyof T]][];
};
```

そしてクラスと一緒にこの関数を使うと！

```tsx:typescript
entries<ScoreClass>(taroGrade).forEach(([key, value]) => {
  if (key === 'english') {
    if (value > 79) console.log('英語の点数が平均を上回ったよ！');
    else console.log('英語の点数は平均以下だね…');
  }
});
```

成績オブジェクトをテスト点数オブジェクトとして扱わないで！とエラーを出してくれるようになりました。

```bash
error TS2345: Argument of type 'GradeClass' is not assignable to parameter of type 'ScoreClass'.
```

もし`<ScoreClass>`の箇所を`<GradeClass>`と書き間違えても、型で定義した場合と同様のエラーを吐いてくれます。

```bash
error TS2365: Operator '>' cannot be applied to types 'string' and 'number'.
```

これで安全にObject.entriesを使えるようになりました！

## まとめ

クラスとObject.entries（Object.keysやObject.valuesも同様）のコラボが発生したときは型付け関数を定義しよう！

最後に、今回は内部の仕組みまできちんと理解せずにハウツーのみを述べる形となってしまいました。有識者のみなさまからのコメントお待ちしております。