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
}

キーワード