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) // OK2 つ目は、プロトタイプ汚染の可能性があるからである。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)
}キーワード
- 構造的型付け
- プロトタイプ汚染
- 型アサーション
- TypeScript