はじめに
業務でTypeScriptを使い始めて少しずつ勉強中なのですが、先日タイトルの通りオブジェクトの値を動的に取得するのに苦戦したので、備忘のためにその手法を残しておきます。
前置きがかなり長いので、やりかただけ確認したい場合は「対処方法」からご確認ください。
前提 - オブジェクトの値の取得方法の話
TypeScriptの話の前に、そもそものJavaScriptの機能としてオブジェクトの値を取得する方法が2種類あります。
一般的には以下のようにオブジェクト名.プロパティ名
のように「.(ドット)」を使用します。
これはドット記法やドット表記法と呼ばれます。
const obj = {
firstName: "John",
age: 25,
country: "America",
};
console.log(obj.firstName); // John
もう一つがオブジェクト名[プロパティ名]
のように「[](ブラケット)」を使って値を取得する方法です。ブラケット記法やブラケット表記法と呼ばれます。
プロパティ名は文字列で指定する必要があります。
const obj = {
firstName: "John",
age: 25,
country: "America",
};
console.log(obj['firstName']); // John
const hoge = 'age';
console.log(obj[hoge]); // 25
ブラケット記法のいいところは、hoge
の例のように変数を指定できることです。
これにより、動的にオブジェクトの値を取得することができます。
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
について、各機能の権限がどうなっているのか動的に参照しようとしていました。
今回は簡易化のため各権限をコンソールへ出力することにします。
失敗した方法
const printPermission = (op:string) => {
console.log(`${op}:${memberPermisson[op]}`);
};
for (const op of operations) {
printPermission(op);
}
型 'string' の式を使用して型 'OperatePermission' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
型 'string' のパラメーターを持つインデックス シグネチャが型 'OperatePermission' に見つかりませんでした。ts(7053)
これはオブジェクトのプロパティが文字列リテラルで定義されており、string
型の変数op
では値を取得できないためエラーとなっています。
これだけだとなんのことかイマイチわからないと思うので、最後におまけでオブジェクトのプロパティとエラーメッセージについて補足しています。
対処方法
keyof
演算子を使用します。
const printPermission = (op: keyof OperatePermission) => {
console.log(`${op}:${memberPermisson[op]}`);
};
for (const op of operations) {
printPermission(op as keyof OperatePermission);
}
keyof
演算子はオブジェクトのプロパティだけをまとめて取得して、リテラルのユニオン型として設定することができます。
つまり、以下のように書くのと同じです。
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
を使ってキャスト必要もないので、より型安全なコードになります。
// 機能一覧
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をずっと書いていた私としては結構好きな言語になってきています。
これからも型について学んでいきます。
おまけ
オブジェクトのプロパティについての補足
オブジェクトのプロパティはリテラル型で定義されます。
const memberPermisson: OperatePermission = {
create: false,
read: true,
update: false,
delete: false,
};
今回の例のようなオブジェクトの場合、プロパティはcreate
、read
、update
、delete
の文字列リテラルです。
普通の文字列ではなく固定文字列であるため、これ以外は受け付けません。
これがどういうことを表すのかを確認するため、以下のような取得例を考えます。
// 直接文字列リテラルを指定
memberPermisson['create'];
// 変数に文字列リテラルを設定
const strRead = 'read';
memberPermisson[strRead];
// 変数にプロパティの文字列を設定
let strUpdate:string;
strUpdate = 'update';
memberPermisson[strUpdate];
どれも正しく値を取得できそうですが、strUpdate
だけエラーになります。
失敗した方法で確認したものと同じエラーです。
strRead
とstrUpdate
も変数に文字列を設定しており、一見同じことをしているように見えます。
しかし、strRead
はエラーになっていません。
実は2つの型は全く別のものとして設定されています。
strRead
の型はリテラル型で、固定文字列として'read'が設定されています。
このようなリテラル型の場合、'read'というような文字列自体が型として設定されているため、'read'という値が確実に設定されることがわかります。
型チェックの際、型がstringであることはわかりますが、中身が何であるかまではわかりません。
オブジェクトのプロパティに存在しない値が入っている可能性があるため、プロパティを取得するための変数としては不適切です。
よって、オブジェクトのプロパティを取得にブラケット記法を使う場合、変数はリテラル型である必要があります。
エラーメッセージについての補足
エラーメッセージの確認
エラーメッセージを理解するには、上記のオブジェクトのプロパティだけではまだ知識が不足しています。
まずエラーメッセージをもう一度確認します。
型 'string' の式を使用して型 'OperatePermission' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
型 'string' のパラメーターを持つインデックス シグネチャが型 'OperatePermission' に見つかりませんでした。ts(7053)
ここでポイントになるのは「インデックス シグネチャ」なのですが、まずは一つずつ理解していきます。
型 'string' の式を使用して型 'OperatePermission' にインデックスを付けることはできない
これはstring
型ではobj[var]
のようなブラケット記法を使ったインデックスでのパラメータ取得ができないということを言っています。
string
型ではプロパティの存在が保証されず、アクセスできないということです。
要素は暗黙的に 'any' 型になります。
上記の理由により、要素の取得が不確定になるので型の推論ができずany
型になってしまうということです。
型 'string' のパラメーターを持つインデックス シグネチャが型 'OperatePermission' に見つかりませんでした。
ここが最大のポイントです。個人的にはこのメッセージを先に持ってくるべきだと思うのですが……
インデックスシグネチャ
インデックスシグネチャとは以下のようなオブジェクトの型の指定方法を指します。
interface obj {
[key: string]: boolean;
}
オブジェクトの型を定義する際、通常はプロパティ名:型
としますが、プロパティ名を明示せず自由に設定する場合、このインデックスシグネチャを使います。
key
の部分は変数なので任意の値を使えます。key
とするのが一般的です。
これはオブジェクトのプロパティを定義する部分で、ここでは任意のプロパティに対しstring
型を定義しています。
そして、値の型をboolean
型としています。
これがエラーメッセージ内で言っている「string
の型を持つインデックス シグネチャ」です。
インデックスシグネチャのプロパティの型にはstring
型のほかnumber
型、symble
型を定義できます。
インデックスシグネチャを使用した例
本記事のコードでは以下のようにオブジェクトの型を定義していました。
// オブジェクトの型定義
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
型でインデックスを指定することはできませんでした。
インデックスシグネチャを使用すると以下のようになります。
// オブジェクトの型定義
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
になってしまいます。
これはバグの温床になるため、インデックスシグネチャを使用する場合は値の取得結果をチェックするなど、バグを生まない対策が必要です。