avoid-numeric-index について
JavaScript のオブジェクトキーの実態とずれる場合がある、独自の数値インデックスシグネチャについて解説する TypeScript スキルです。用途に応じて配列、タプル、ArrayLike、Iterable、Map を使い分けます。
主な機能
- 01独自の数値インデックスシグネチャを避ける
- 02シーケンスには配列を使用
- 03固定長データにはタプルを使用
- 04アクセス方法に応じて ArrayLike または Iterable を選択
- 05疎な数値キーには Map を使用
活用シーン
- 01配列風の TypeScript 型の定義
- 02固定長の座標や色の表現
- 03疎な数値キーのデータ処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill avoid-numeric-indexインストール後、Claude Code で/avoid-numeric-indexを入力して読み込まれたか確認します。
詳細
数値インデックスシグネチャを避ける
概要
JavaScript のオブジェクトキーは、配列でも常に文字列です。
TypeScript の数値インデックスシグネチャは、間違いを検出するのに役立つ便宜的な仕組みですが、JavaScript の実行時の動作を反映していません。代わりに、Array、タプル、または ArrayLike 型を使用してください。
このスキルを使う場面
- 配列のような型を定義するとき
- JavaScript の配列の動作を理解するとき
- 配列とオブジェクトのどちらを使うか選ぶとき
- 配列に対して Object.keys を使うとき
絶対に守るルール
NEVER use number as an index signature type.
Use Array<T>, tuple, or ArrayLike<T> instead.
覚えておきましょう:
- JavaScript はすべてのオブジェクトキーを文字列に変換します
- 配列インデックスは実行時には文字列です
Object.keysは常に文字列を返します- TypeScript の数値インデックスはコンパイル時のみのものです
検証:JavaScript の実態
// JavaScript converts numeric keys to strings
const obj = { 1: 'one', 2: 'two' };
console.log(Object.keys(obj)); // ['1', '2'] - strings!
// Arrays work the same way
const arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // ['0', '1', '2'] - strings!
// String access works on arrays
console.log(arr['1']); // 'b' - same as arr[1]
TypeScript の便宜的な仕組み
TypeScript では、配列に数値インデックスがあるものとして扱います:
interface Array<T> {
[n: number]: T; // Fiction: indices are actually strings
}
const xs = [1, 2, 3];
const x0 = xs[0]; // OK: number index
const x1 = xs['1']; // OK: TypeScript allows stringified numbers
// TypeScript catches non-numeric string indices
const inputEl = document.querySelector('input')!;
const bad = xs[inputEl.value];
// ~~~~~~~~~~~~~~~~~
// Index expression is not of type 'number'.
技術的には正確ではありませんが、間違いを検出するのに役立ちます。
数値インデックスシグネチャを避ける理由
実態が表面化する
const xs = [1, 2, 3];
const keys = Object.keys(xs);
// ^? const keys: string[] (not number[]!)
for (const key of Object.keys(xs)) {
console.log(typeof key); // 'string', always
}
誤ったメンタルモデルを生み出す
// This might make you think numeric keys are "real"
type NumericDict = { [key: number]: string };
// But at runtime:
const dict: NumericDict = { 0: 'zero', 1: 'one' };
console.log(Object.keys(dict)); // ['0', '1'] - strings!
より適切な代替手段
シーケンスには Array を使う
// Instead of
type Bad = { [index: number]: string };
// Use
type Good = string[]; // or Array<string>
長さが固定の場合はタプルを使う
// Fixed-length numeric "index"
type Point = [number, number]; // 2 elements
type RGB = [number, number, number]; // 3 elements
const origin: Point = [0, 0];
配列のような構造には ArrayLike を使う
// ArrayLike has length and numeric index signature
// Good for accepting any indexable collection
function sum(items: ArrayLike<number>): number {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i];
}
return total;
}
// Works with arrays
sum([1, 2, 3]);
// Works with array-like objects
sum({ 0: 1, 1: 2, 2: 3, length: 3 });
// Works with NodeList, arguments, etc.
反復処理のみを行う場合は Iterable を使う
// If you only need to iterate, not index
function sumIterable(items: Iterable<number>): number {
let total = 0;
for (const item of items) {
total += item;
}
return total;
}
// Works with arrays
sumIterable([1, 2, 3]);
// Works with Sets
sumIterable(new Set([1, 2, 3]));
// Works with generators
function* nums() { yield 1; yield 2; yield 3; }
sumIterable(nums());
文字列インデックスと数値インデックスの関係
// When you use a number index, you get T
// When you use Object.keys, you get string[]
// This inconsistency is intentional
const arr = [1, 2, 3];
// TypeScript: number index gives number
const first: number = arr[0]; // OK
// JavaScript reality: keys are strings
const keys = Object.keys(arr); // string[]
// You can use string indices (TypeScript allows numeric strings)
const second = arr['1']; // OK, TypeScript permits this
安全な配列アクセス
// Without noUncheckedIndexedAccess:
const arr = [1, 2, 3];
const x = arr[10];
// ^? const x: number (but actually undefined!)
// With noUncheckedIndexedAccess:
const y = arr[10];
// ^? const y: number | undefined (safer)
// Or use a wrapper function:
function checkedAccess<T>(xs: ArrayLike<T>, i: number): T {
if (i >= 0 && i < xs.length) {
return xs[i];
}
throw new Error(`Index ${i} out of bounds`);
}
圧力に負けないためのプロトコル
1. 「疎配列の型が欲しい」
圧力:「疎配列には { [n: number]: T } が必要です」
**回答:**疎なデータには Map<number, T> を使うか、通常の配列でオプショナルアクセスを使ってください。
**アクション:**疎な数値キーには new Map<number, string>() を使います。
2. 「オブジェクトに数値キーが必要」
圧力:「オブジェクトでは ID をキーとして使います:{ 1: user1, 2: user2 }」
回答:Map<number, User> または Record<string, User> を使い、必要に応じて変換してください。
**アクション:**キーが文字列であることを受け入れるか、Map を使います。
危険信号 — 立ち止まって再考する
- 独自の型で
{ [key: number]: T }を使っている Object.keys(array)が数値を返すと思い込んでいる- 数値文字列を配列インデックスとして使っている
typeof keyが 'string' になる理由がわからない
よくある誤った正当化
| 言い訳 | 実態 |
|---|---|
| 「配列には数値インデックスがある」 | 実行時には文字列です |
| 「TypeScript は Array で number を使う」 | 型安全性のための便利な便宜的仕組みです |
| 「疎な数値キーが必要」 | 代わりに Map<number, T> を使います |
クイックリファレンス
// DON'T: Numeric index signature
type Bad = { [n: number]: string };
// DO: Use Array for sequences
type Good = string[];
// DO: Use tuple for fixed length
type Point = [number, number];
// DO: Use ArrayLike for indexable collections
function process(items: ArrayLike<string>) { ... }
// DO: Use Iterable for anything you can loop over
function process(items: Iterable<string>) { ... }
// DO: Use Map for sparse numeric keys
const sparse = new Map<number, string>();
要点
数値インデックスシグネチャは TypeScript の便宜的な仕組みであり、JavaScript の実態ではありません。
JavaScript のオブジェクトキーは常に文字列です。TypeScript の数値インデックスは間違いの検出に役立ちますが、誤ったメンタルモデルを生み出します。独自の数値インデックスシグネチャを定義する代わりに、Array、タプル、ArrayLike、または Map を使ってください。
参考資料
Dan Vanderkam 著「Effective TypeScript」、項目 17「数値インデックスシグネチャを避ける」に基づいています。