🧷

【TypeScript】Object.keys() の返り値をstring[]型でなくユニオン型の配列にしたい

2023/02/16に公開

課題

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()に渡した関数の返り値の型注釈でエラーが発生していることが確認できると思います。

https://www.typescriptlang.org/play?#code/MYewdgzgLgBADgSwNYEMYF4YG8woLYCmAXDAOSArDINUMggwyCnDIGUMpANDFAJ5zEwDapg5gyBkhkCyDKQC6AXwBQkgPIAjAFYFgUAHRICbCAApEqAJSq8KONu0a2+ktABOCMAHMMAPmySYMCAHcEUYAAsYc019Nw8PYBQIAjJcQlIid3CPGwIoAFcbMDJAWBVAWSVSAG4kiKiY0nZOBJLw1IysskB+hkAShkB1hiKkqXF9IA

コンパイラはmap()に渡した関数の引数keystring型であると推論するため、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節を一つ消すとコンパイルエラーが発生することを確認できると思います。

https://www.typescriptlang.org/play?#code/MYewdgzgLgBA5gUygaQQTwjAvDAPAFRgQA8oEwATTAbwG0BrdALhmgCcBLMOAXRYFcw9MCADuYAL4A+ABQgARgCsW+AJQsZjNCABmMNbR7YpMagCgYMNkn5swMAPJKEwKADotEOUtVmJZs1BIWAAHDnoAQ2xTMAiAWwQWAHJAFYZAaoZAQYZAU4ZAMoYkgBoYKDQQxJhaJMBzBkBkhkBZBiSefzNEFHQvMMjVNziIkJlNdHVWKE5uY1MLGAB6KZgtZNiEpJgAHxgk4tKkyYhRDihgAAsYAbRVCctLGZhgCIgEQBkGQCbfQFD-QFUGQBiGQDMGQCsGQBEGQAWDIBmhkAzwyARYZACUMgGuGQAVDIBLhkAPwwPQBeboB8V0AmgyAaIZJpZbvd1osEEkmDjLtYoLZ7ElALAqgFklJIAblJeIQ602RJJlzJNjs60A-QwQwDrDIzJv4JF0dCA2ABRCJHfrsIYANxAHAo43MuPAEBAABsEG5dSA4DJ2DAANTrQBJDIBaqMA-gyANeVAMYMgH0GQARDIBAf6SqiZ4rMQA

おまけ(より深く知りたい方へ)

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}に制限することでこの問題を解決しています。

さらに詳しい内容は以下のリンク等を参考にしてください。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
https://www.typescriptlang.org/docs/handbook/2/keyof-types.html

Discussion