TL;DR
- JavaScript のオブジェクトのキーは文字列かシンボルであり、配列のインデックスも実際には文字列である。
- 数値のインデックスシグネチャは誤解を招くので避け、
Array、タプル型、ArrayLike、Iterableを使う。
内容
JavaScript のキーは文字列
JavaScript のオブジェクトのキーは、文字列かシンボルである。他の言語にあるような「ハッシュ可能(hashable)」という概念はない。文字列やシンボル以外の値をキーにしようとすると、toString メソッドが呼ばれて文字列に変換される。
配列もオブジェクトなので、インデックスは文字列に変換されている。そのため、文字列でも要素にアクセスできる。
x = [1, 2, 3]
x["1"] // 2
Object.keys(x) // ['0', '1', '2']TypeScript でのモデリング
TypeScript は、数値のキーを許し、文字列のキーと区別することで、これをモデリングしている。es5.d.ts では、Array は次のように定義されている。
interface Array<T> {
// ...
[n: number]: T
}そのため、配列を文字列でインデックスすると型エラーになる。ただし、'1' のような数値の文字列は暗黙に数値として扱われるので、xs['1'] はエラーにならない。
const xs = [1, 2, 3]
const inputEl = document.getElementsByTagName("input")[0]
xs[1] // OK
const xN = xs[inputEl.value] // inputEl.value は string なので型エラーしかし、実体は依然として文字列である(項目3)。Object.keys(xs) は ['0', '1', '2'] を返す。
数値のインデックスシグネチャの代わり
以上から、数値のキーが存在するという誤解を読み手に与えかねないので、インデックスシグネチャに number を使うのは避けるべきである。数値でインデックスしたいときは、代わりに Array<T>、タプル型、ArrayLike<T>、Iterable<T> を使う。
push や concat のような破壊的な操作が不要なら、ArrayLike<T> を使う。ArrayLike<T> は、length と数値のインデックスシグネチャだけを持つ型である。
function checkedAccess<T>(xs: ArrayLike<T>, i: number): T {
if (i >= 0 && i < xs.length) return xs[i]
throw new Error(`Attempt to access ${i} which is past end of array.`)
}単に反復処理ができればよいなら、Iterable<T> を使う。関数の引数をこの型にすれば、ジェネレータも渡せるようになる(項目30)。
function sum(xs: Iterable<number>): number {
let sum = 0
for (const x of xs) {
sum += x
}
return sum
}