疑問
TypeScriptでObject.keysを使う際keyの型がなぜstringの配列になってしまうのか気になったのでまとめました。
Object.keys
でobjectのkey名を取り出したいとき
次のようなコードを書くとkeysの型はstring[]
になってしまいます。
const object = {
hoge: 'hoge',
fuga: 'fuga',
};
// keysの型は("hoge"|"fuga")[]ではなくstring[]
const keys = Object.keys(object);
よって次のようなコードは型エラーになってしまいます。
// 型エラーになってしまう
const temp = object[keys[0]];
暫定的解決策
これを回避するためには(keyof T)[]
を返す関数を作ってそれを利用する。もしくはasを使って型を強制的に決めてしまうことで対処できます。
function getStrictKeys<T extends Record<string, any>>(object: T): (keyof T)[] {
return Object.keys(object);
}
// keysの型は ("hoge" | "fuga")[]
const keys = getStrictKeys(object);
// 型エラーなし
const temp = object[keys[0]];
// keys[0]の型を無理矢理つける
const temp = object[keys[0] as keyof typeof object];
最初からObject.keys
の型を(keyof object)[]
にしてくれれば良いのにと思ったのですが
なぜこのような仕様になっているかというと、objectが拡張された時に型安全性が失われてしまうからという理由があるためです。
Object.Keysの型を厳密にすることで発生する問題
具体的には次のような型を考えます
type ObjectType = {
hoge: string;
fuga: string;
};
次のような関数を考えます
function printObjectKeys(object: ObjectType) {
getStrictKeys(object).forEach((key) => {
// 型補完でkeyの型はhoge|fuga
if (key !== 'hoge' && key !== 'fuga') {
throw new Error(`keyがhogeでもfugaでもありません:${key}`);
}
console.log(key);
});
}
この関数はObjectTypeのkeyを出力するだけでkeyの型がhoge|fuga
なので型安全性が保たれていればif文の中には入らないように思えます。
しかし、次のようなObjectType
を拡張したようなobjectを用意し
const extendObject = {
hoge: 'hoge',
fuga: 'fuga',
piyo: 'piyo',
};
printObjectKeys
関数に引数として渡すとなんの静的エラーも出ず実行されてしまいます。
そしてif文の中に入りエラーが発生してしまいます。
// Error: keyがhogeでもfugaでもありません:piyo
printObjectKeys(extendObject);
このようにObject.keys
の型を厳格に付けてしまうとかえって静的エラーが出ずtypescriptの良さが失われてしまいます。
所感
Object.keys
の型を厳密にする関数の使用やas
による型アサーションはtypescriptの型安全性を失わせる可能性があるのでobjectが拡張される可能性がないと言い切れる時など注意して使う必要があります。