TL;DR

  • オブジェクトを反復処理すると、型を指定していてもキーの型が string と推論されることが多い。
  • キーと値の両方を反復処理するなら Object.entries を使う。キーの型を絞りたいなら、キーを明示的に列挙するか Map を使う。

内容

キーが string と推論される理由

オブジェクトを反復処理すると、型を指定していてもキーの型が string と推論されることがよくある。主な理由は 2 つある。

1 つ目は、TypeScript が構造的型付けを採用しているので、関数のパラメータとして受け取るオブジェクトが型にないキーを含んでいる可能性があるからである。

const foo = (abc: ABC) => {
  for (const k in abc) {
    const v = abc[k]
    //        ~~~~~~ Element implicitly has an 'any' type because ...
  }
  // k の型は string
}
 
const x = { a: "1", b: "2", c: 3, d: new Date() }
foo(x) // OK

2 つ目は、プロトタイプ汚染の可能性があるからである。Object.prototype に定義したプロパティは、他のすべてのオブジェクトに継承される。継承したプロパティは for-in ループで列挙されるので、TypeScript は安全側に倒してキーの型を string と推論する。

Object.entries を使う

Object.entries は継承したプロパティを含まない。そのため、キーと値の両方を反復処理するなら Object.entries を使うのがよい。

const foo = (abc: ABC) => {
  for (const [k, v] of Object.entries(abc)) {
    //          ^? const k: string
    console.log(v)
    //          ^? const v: any
  }
}

ただし、キーの型は string、値の型は any になるので扱いづらい(項目30 入力には寛容に、出力には厳格に)。

その他の回避策

オブジェクトがイミュータブルだと分かっているなら、for-in ループの中でキーの型を指定する。ミュータブルな場合は、実行時のキーと値が型と食い違うことがあり、混乱のもとになるので避ける(項目9 型アノテーションを型アサーションより優先的に使用する)。

for (const kStr in abc) {
  let k = kStr as keyof ABC
  //  ^? let k: keyof ABC (= 'a' | 'b' | 'c')
  const v = abc[k]
  //    ^? const v: string | number
}

必要なキーを明示的に列挙する方法もある。

const keys = ["a", "b", "c"] as const
for (const k of keys) {
  //       ^? const k: 'a' | 'b' | 'c'
  const v = abc[k]
  //    ^? const v: string | number
}

オブジェクトの代わりに Map を使う方法もある(項目16 インデックスシグネチャよりも適切な代替手段を使う)。

const m = new Map([
  ["one", "uno"],
  ["two", "dos"],
  ["three", "tres"],
])
//    ^? const m: Map<string, string>
 
for (const [k, v] of m.entries()) {
  // k: string, v: string
  console.log(v)
}

キーワード