Effective TypeScript

使用更精确的替代方案而不是索引签名

在 TypeScript 中,Index Signatures(索引签名) 允许定义对象可以包含任意数量的 key,但同时也牺牲了类型系统的精确性和安全性。本节主要阐述为何应避免滥用索引签名,以及在不同场景下更优的替代方案。

什么是索引签名

索引签名的语法如下:

type Rocket = { [property: string]: string };

const rocket: Rocket = {
  name: 'Falcon 9',
  variant: 'v1.0',
  thrust: '4,940 kN',
};

它包含三个含义:

  1. Key 的名称(如 property):仅用于文档,对类型检查无影响。
  2. Key 的类型:必须是 stringnumbersymbol 的子类型。
  3. Value 的类型:可以是任意类型。

索引签名的缺点

虽然灵活,但它带来显著的缺陷:

  1. 允许任意 Key:拼写错误不会报错(如写成 Name 而不是 name)。
  2. 允许空对象:不强制要求包含任何特定 Key,{} 也是合法的。
  3. 类型单一:所有 Key 对应的值必须是相同类型(或其联合类型),无法区分不同字段的不同类型(如 thrust 应该是 number 而不是 string)。
  4. 开发体验差:IDE 无法提供自动补全(Autocomplete)。

替代方案与最佳实践

对于已知结构的某些字段:使用 Interface

如果清楚对象应该具备哪些字段,应显式定义 Interface,而非使用索引签名。

不推荐:

type Rocket = { [property: string]: string };

推荐:

interface Rocket {
  name: string;
  variant: string;
  thrust_kN: number;
}

这样可以获得类型检查、拼写检查和 IDE 补全支持。

对于真正的动态数据:使用 Map

如果数据确实是动态的(例如解析 CSV 文件,列名在运行时才确定),索引签名是一种选择,但 Map 通常更好。

索引签名方式(存在原型链污染等隐患):

function parseCSV(input: string): { [columnName: string]: string }[] {
  // ... 实现略
  return rows;
}

Map 方式(更安全):

function parseCSVMap(input: string): Map<string, string>[] {
  // ... 使用 new Map()
  return rows;
}

使用 Map 的优势在于明确区分了“已知属性的对象”和“键值对集合”。如果后续需要将数据转换为具体类型,应编写一个解析函数并在其中进行运行时检查:

function parseRocket(map: Map<string, string>): Rocket {
  // 运行时验证数据完整性
  const name = map.get('name');
  const variant = map.get('variant');
  // ...
  if (!name || !variant) throw new Error('Invalid rocket');
  return { name, variant, ... };
}

对于受限的动态 Key:使用 Record

如果 Key 不是无限的 string,而是字符串的子集,应避免使用宽泛的索引签名。

场景:例如只允许 x, y, z 作为 Key。

推荐:使用 Record 或 映射类型

type Vec3D = Record<'x' | 'y' | 'z', number>;
// 等同于:
// type Vec3D = {
//   x: number;
//   y: number;
//   z: number;
// }

混合使用:已知字段 + 额外字段

如果需要通过类型检查来允许对象包含额外属性(以此禁用 Excess Property Checking),可以结合使用具体字段和索引签名。

interface ButtonProps {
  title: string;
  onClick: () => void;
  // 允许其他任意 string key,但不仅限于此
  [otherProps: string]: unknown;
}

const props: ButtonProps = {
  title: 'Submit',
  onClick: () => {},
  'data-id': '123' // 合法
};

注意,索引签名的值类型必须兼容所有已知字段的类型。如果已知字段类型各异,索引签名的值通常只能是 anyunknown

总结

  • 识别缺陷:索引签名会导致类型安全丧失、自动补全失效以及无法区分字段类型。
  • 优先精准:尽可能使用 interfaceRecord 或映射类型来描述已知结构。
  • 处理动态数据:对于运行时确定的键值对,优先考虑使用 Map
  • 受限 Key:当 Key 是特定字符串集合时,使用 Record 而非宽泛的 string 索引。
在 GitHub 上编辑

上次更新于