mcpField
A
avoid-numeric-index

avoid-numeric-index

github.com/marius-townhouse開発642 回インストール38 週間前に更新
リポジトリ

avoid-numeric-index について

JavaScript のオブジェクトキーの実態とずれる場合がある、独自の数値インデックスシグネチャについて解説する TypeScript スキルです。用途に応じて配列、タプル、ArrayLike、Iterable、Map を使い分けます。

主な機能

  1. 01独自の数値インデックスシグネチャを避ける
  2. 02シーケンスには配列を使用
  3. 03固定長データにはタプルを使用
  4. 04アクセス方法に応じて ArrayLike または Iterable を選択
  5. 05疎な数値キーには Map を使用

活用シーン

  1. 01配列風の TypeScript 型の定義
  2. 02固定長の座標や色の表現
  3. 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「数値インデックスシグネチャを避ける」に基づいています。

コメント