Effective TypeScript
使用更精确的替代方案而不是索引签名
在 TypeScript 中,Index Signatures(索引签名) 允许定义对象可以包含任意数量的 key,但同时也牺牲了类型系统的精确性和安全性。本节主要阐述为何应避免滥用索引签名,以及在不同场景下更优的替代方案。
什么是索引签名
索引签名的语法如下:
type Rocket = { [property: string]: string };
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: '4,940 kN',
};它包含三个含义:
- Key 的名称(如
property):仅用于文档,对类型检查无影响。 - Key 的类型:必须是
string、number或symbol的子类型。 - Value 的类型:可以是任意类型。
索引签名的缺点
虽然灵活,但它带来显著的缺陷:
- 允许任意 Key:拼写错误不会报错(如写成
Name而不是name)。 - 允许空对象:不强制要求包含任何特定 Key,
{}也是合法的。 - 类型单一:所有 Key 对应的值必须是相同类型(或其联合类型),无法区分不同字段的不同类型(如
thrust应该是number而不是string)。 - 开发体验差: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' // 合法
};注意,索引签名的值类型必须兼容所有已知字段的类型。如果已知字段类型各异,索引签名的值通常只能是 any 或 unknown。
总结
- 识别缺陷:索引签名会导致类型安全丧失、自动补全失效以及无法区分字段类型。
- 优先精准:尽可能使用
interface、Record或映射类型来描述已知结构。 - 处理动态数据:对于运行时确定的键值对,优先考虑使用
Map。 - 受限 Key:当 Key 是特定字符串集合时,使用
Record而非宽泛的string索引。
在 GitHub 上编辑
上次更新于