TypeScriptでオブジェクトの値をブラケット記法で動的に取得する

## はじめに
業務でTypeScriptを使い始めて少しずつ勉強中なのですが、先日タイトルの通りオブジェクトの値を動的に取得するのに苦戦したので、備忘のためにその手法を残しておきます。

前置きがかなり長いので、やりかただけ確認したい場合は「[対処方法](#対処方法)」からご確認ください。

## 前提 - オブジェクトの値の取得方法の話
TypeScriptの話の前に、そもそものJavaScriptの機能としてオブジェクトの値を取得する方法が2種類あります。
一般的には以下のように`オブジェクト名.プロパティ名`のように「.(ドット)」を使用します。
これはドット記法やドット表記法と呼ばれます。
```javascript:ドット記法
const obj = {
  firstName: "John",
  age: 25,
  country: "America",
};

console.log(obj.firstName); // John
```

もう一つが`オブジェクト名[プロパティ名]`のように「\[](ブラケット)」を使って値を取得する方法です。ブラケット記法やブラケット表記法と呼ばれます。
プロパティ名は文字列で指定する必要があります。

```javascript:ブラケット記法
const obj = {
  firstName: "John",
  age: 25,
  country: "America",
};

console.log(obj['firstName']); // John

const hoge = 'age';
console.log(obj[hoge]); // 25
```

ブラケット記法のいいところは、`hoge`の例のように変数を指定できることです。
これにより、動的にオブジェクトの値を取得することができます。

## TypeScriptだとどうなるのか
### やりたかったこと
業務で使ってるコードはそのまま出せないので、ざっくり以下のような権限管理をしているオブジェクトがあるとします。

```typescript
// 機能一覧
const operations = ["create", "read", "update", "delete"];

// 各機能に対する権限
interface OperatePermission {
  create: boolean;
  read: boolean;
  update: boolean;
  delete: boolean;
}

// メンバー管理の権限を設定
const memberPermisson: OperatePermission = {
  create: false,
  read: true,
  update: false,
  delete: false,
};
```
この`memberPermission`について、各機能の権限がどうなっているのか動的に参照しようとしていました。

今回は簡易化のため各権限をコンソールへ出力することにします。

### 失敗した方法
```typescript
const printPermission = (op:string) => {
  console.log(`${op}:${memberPermisson[op]}`);
};

for (const op of operations) {
  printPermission(op);
}
```
一見これでよさそうですが、関数内で型のエラーがでます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/c3a983f2-f217-ff51-b10b-74fbfedb247c.png)

```plaintext:エラーメッセージ抜粋
型 'string' の式を使用して型 'OperatePermission' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
  型 'string' のパラメーターを持つインデックス シグネチャが型 'OperatePermission' に見つかりませんでした。ts(7053)
```
これはオブジェクトのプロパティが文字列リテラルで定義されており、`string`型の変数`op`では値を取得できないためエラーとなっています。

これだけだとなんのことかイマイチわからないと思うので、最後に[おまけ](#おまけ)でオブジェクトのプロパティとエラーメッセージについて補足しています。

### 対処方法
`keyof`演算子を使用します。

```typescript
const printPermission = (op: keyof OperatePermission) => {
  console.log(`${op}:${memberPermisson[op]}`);
};

for (const op of operations) {
  printPermission(op as keyof OperatePermission);
}
```

`keyof`演算子はオブジェクトのプロパティだけをまとめて取得して、リテラルのユニオン型として設定することができます。
つまり、以下のように書くのと同じです。
```typescript
const printPermission = (op: "create" | "read" | "update" | "delete") => {
  console.log(`${op}:${memberPermisson[op]}`);
};
for (const op of operations) {
  printPermission(op as "create" | "read" | "update" | "delete");
}
```
プロパティが多くなってくると、上記のように一つずつ列挙するわけにはいかないので、`keyof`演算子は非常に便利です。

このように`keyof`演算子で変数の型がリテラル型であることを保証することにより、変数をブラケット記法で使えるようになります。

なお、`keyof`演算子は型定義ができる場所ならどこにでも使えるため、`operations`の配列の型自体を`keyof`演算子で定義することもできます。

そうするとわざわざ`as`を使ってキャスト必要もないので、より型安全なコードになります。

```typescript:配列の型をkeyofで指定
// 機能一覧
const operations: (keyof OperatePermission)[] = [
  "create",
  "read",
  "update",
  "delete",
];

// 中略

const printPermission = (op: keyof OperatePermission) => {
  console.log(`${op}:${memberPermisson[op]}`);
};

for (const op of operations) {
  printPermission(op);
}
```


## まとめ
型の制約を受けるため、ブラケット記法を使うにはちょっとした注意が必要なことがわかりました。
また、`keyof`演算子を使うことで、より型安全で保守性の高いコードを書けることがわかりました。
TypeScriptは型のエラーに苦しめられることが多いですが、裏を返せば型をしっかり固めることでバグが少なく保守性の高いコードが書けるので、Javaをずっと書いていた私としては結構好きな言語になってきています。

これからも型について学んでいきます。

## おまけ
### オブジェクトのプロパティについての補足
オブジェクトのプロパティはリテラル型で定義されます。
```typescript
const memberPermisson: OperatePermission = {
  create: false,
  read: true,
  update: false,
  delete: false,
};
```
今回の例のようなオブジェクトの場合、プロパティは`create`、`read`、`update`、`delete`の文字列リテラルです。
普通の文字列ではなく固定文字列であるため、これ以外は受け付けません。

これがどういうことを表すのかを確認するため、以下のような取得例を考えます。
```typescript
// 直接文字列リテラルを指定
memberPermisson['create'];

// 変数に文字列リテラルを設定
const strRead = 'read';
memberPermisson[strRead];

// 変数にプロパティの文字列を設定
let strUpdate:string;
strUpdate = 'update';
memberPermisson[strUpdate];
```

どれも正しく値を取得できそうですが、`strUpdate`だけエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/27dfdef2-41b1-0e18-1bf0-f21480855084.png)

失敗した方法で確認したものと同じエラーです。

`strRead`と`strUpdate`も変数に文字列を設定しており、一見同じことをしているように見えます。
しかし、`strRead`はエラーになっていません。
実は2つの型は全く別のものとして設定されています。

`strRead`の型はリテラル型で、固定文字列として'read'が設定されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/552a9b8e-1006-46ce-df78-cf99b6b3847c.png)

このようなリテラル型の場合、'read'というような文字列自体が型として設定されているため、'read'という値が確実に設定されることがわかります。

一方、`strUpdate`はあくまでもstring型です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/5275c509-7220-130f-9c75-ec2763f9d76d.png)

型チェックの際、型がstringであることはわかりますが、中身が何であるかまではわかりません。
オブジェクトのプロパティに存在しない値が入っている可能性があるため、プロパティを取得するための変数としては不適切です。

よって、オブジェクトのプロパティを取得にブラケット記法を使う場合、変数はリテラル型である必要があります。

### エラーメッセージについての補足
#### エラーメッセージの確認
エラーメッセージを理解するには、上記のオブジェクトのプロパティだけではまだ知識が不足しています。

まずエラーメッセージをもう一度確認します。

```plaintext:エラーメッセージ抜粋
型 'string' の式を使用して型 'OperatePermission' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
  型 'string' のパラメーターを持つインデックス シグネチャが型 'OperatePermission' に見つかりませんでした。ts(7053)
```
ここでポイントになるのは「インデックス シグネチャ」なのですが、まずは一つずつ理解していきます。

`型 'string' の式を使用して型 'OperatePermission' にインデックスを付けることはできない`
これは`string`型では`obj[var]`のようなブラケット記法を使ったインデックスでのパラメータ取得ができないということを言っています。
`string`型ではプロパティの存在が保証されず、アクセスできないということです。

`要素は暗黙的に 'any' 型になります。`
上記の理由により、要素の取得が不確定になるので型の推論ができず`any`型になってしまうということです。

`型 'string' のパラメーターを持つインデックス シグネチャが型 'OperatePermission' に見つかりませんでした。`
ここが最大のポイントです。個人的にはこのメッセージを先に持ってくるべきだと思うのですが……

#### インデックスシグネチャ
インデックスシグネチャとは以下のようなオブジェクトの型の指定方法を指します。

```typescript
interface obj {
  [key: string]: boolean;
}
```
オブジェクトの型を定義する際、通常は`プロパティ名:型`としますが、プロパティ名を明示せず自由に設定する場合、このインデックスシグネチャを使います。

`key`の部分は変数なので任意の値を使えます。`key`とするのが一般的です。
これはオブジェクトのプロパティを定義する部分で、ここでは任意のプロパティに対し`string`型を定義しています。

そして、値の型を`boolean`型としています。

これがエラーメッセージ内で言っている「`string`の型を持つインデックス シグネチャ」です。
インデックスシグネチャのプロパティの型には`string`型のほか`number`型、`symble`型を定義できます。


#### インデックスシグネチャを使用した例
本記事のコードでは以下のようにオブジェクトの型を定義していました。
```typescript
// オブジェクトの型定義
interface OperatePermission {
  create: boolean;
  read: boolean;
  update: boolean;
  delete: boolean;
}

// オブジェクトの設定
const memberPermisson: OperatePermission = {
  create: false,
  read: true,
  update: false,
  delete: false,
};

// 変数にプロパティの文字列を設定
let strUpdate:string;
strUpdate = 'update';
memberPermisson[strUpdate]; // エラー
```
これはリテラル型での定義なので`strUpdate`のような`string`型でインデックスを指定することはできませんでした。

インデックスシグネチャを使用すると以下のようになります。
```typescript
// オブジェクトの型定義
interface OperatePermission {
  [key: string]: boolean;
}

// オブジェクトの設定
const memberPermisson: OperatePermission = {
  create: false,
  read: true,
  update: false,
  delete: false,
};

// 変数にプロパティの文字列を設定
let strUpdate:string;
strUpdate = 'update';
memberPermisson[strUpdate]; // エラーにならず取得可能
```

プロパティはリテラル型ではなく、`string`型で定義されているので問題なく取得できます。

しかし、この方法だともし`strUpdate`がオブジェクトに存在しないプロパティである場合、エラーにはなりませんが取得結果が`undefined`になってしまいます。
これはバグの温床になるため、インデックスシグネチャを使用する場合は値の取得結果をチェックするなど、バグを生まない対策が必要です。