【TypeScript】Object.keys() の返り値をstring[]型でなくユニオン型の配列にしたい
課題
TypeScriptにおけるObject.keys()の型は(o: {}) => string[]
です。すなわち、以下のコードではコンパイルエラーが発生してしまいます。
const pika = {name: 'ピカチュウ', type: ['でんき']}
Object.keys(pika).map((key): string => {
// keyの型はstringと推論される
switch (key) {
case 'name':
return '名前';
case 'type':
return 'タイプ';
}
})
このコードをTypeScript Playgroundで用意しました。map()に渡した関数の返り値の型注釈でエラーが発生していることが確認できると思います。
コンパイラはmap()に渡した関数の引数key
をstring
型であると推論するため、2つのcase節(case 'name'
とcase 'type'
)では引っかからない場合もあると判断してしまいます。ゆえに、map()に渡した関数の返り値はstring | undefined
型と推論され、型注釈と矛盾しコンパイルエラーとなります。
このエラーはswitch文にdefault節を追加することでも解決できますが、可能であればcase節の網羅性を静的にチェックしたいところですよね。そのためには、引数key
が'name' | 'type'
のようなユニオン型に推論されてほしいところです。
解決策
Object.keys()のラッパー関数を定義し、ジェネリック型を利用して型注釈を付けることで解決します。
const getKeys = <T extends {[key: string]: unknown}>(obj: T): (keyof T)[] => {
return Object.keys(obj)
}
この関数は例えば以下のように使えます。
const pika = {name: 'ピカチュウ', type: ['でんき']}
getKeys(pika).map((key): string => {
// key: 'name' | 'type'
switch (key) {
// caseが網羅されてないとコンパイルエラーが発生する
case 'name':
return '名前';
case 'type':
return 'タイプ';
}
}).forEach((str): void => {
console.log(str + 'を読み取ったよ!');
})
このようにすることで、引数key
が'name' | 'type'
型に推論され、switch文においてcase節が網羅されているかどうかを静的にチェックできるようになりました。
このコードもTypeScript Playgroundで用意しました。case節を一つ消すとコンパイルエラーが発生することを確認できると思います。
おまけ(より深く知りたい方へ)
Object.keys()の返り値がユニオン型ではなくstring[]
型になっているのには理由があります。
JavaScriptのオブジェクトは、キーとしてstring
型の値だけでなくnumber
型の値を指定することもできます。実行時、JavaScriptは自動的にnumber
型のキーをstring
型に変換して処理を行います。
以下は、キーにnumberを指定したオブジェクトを渡した時のObject.keys()の挙動です。number
型だったkeyは、string
型に変換されて出力されます。
const obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
このため、Object.keys()の返り値の型として、引数として渡されたオブジェクトのキーを単純に並べたユニオン型は不適となってしまいます。
今回紹介したラッパー関数では、引数のオブジェクトの型を{[key: string]: unknown}
に制限することでこの問題を解決しています。
さらに詳しい内容は以下のリンク等を参考にしてください。
Discussion