mcpField
A
avoid-numeric-index

avoid-numeric-index

github.com/marius-townhouse开发642 次安装38 周前更新
代码仓库

关于 avoid-numeric-index

这项 TypeScript 技能说明自定义数字索引签名可能误导人们理解 JavaScript 对象键,并按数据结构和访问需求推荐数组、元组、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 条:避免使用数字索引签名。

评论

支持 Markdown