一、什么是索引签名?
索引签名就是为对象定义一种"规则",规定了"什么样的键"对应"什么样的值"。
语法如下:
typescript
interface MyObject {
[key: KeyType]: ValueType;
}
// 或者使用 type
type MyType = {
[key: KeyType]: ValueType;
};
key
: 只是一个占位符,你可以取任何名字,比如prop
,index
等。KeyType
: 键的类型。它必须是string
,number
,symbol
或由它们组成的联合类型。因为在JavaScript中,对象的键最终都会被转换成字符串。ValueType
: 值的类型,可以是任何 TypeScript 类型。- 所有成员都必须符合字符串的索引签名
示例
typescript
interface ScoreRecord {
[name: string]: number;
}
const scores: ScoreRecord = {
'alice': 100,
'bob': 95,
'charlie': 98
};
// 访问是类型安全的
const aliceScore: number = scores['alice']; // OK
// 添加新的键值对也必须遵守规则
scores['dave'] = 99; // OK
scores['eve'] = 'A+'; // Error:不能将类型"string"分配给类型"number"
二、注意事项
2.1 可以与其他属性共存,但必须兼容
一个类型可以同时拥有索引签名和明确的属性。但有一个重要前提:所有明确定义的属性,其类型都必须是索引签名值类型的子类型。
typescript
interface UserProfile {
// 明确的属性
id: number;
name: string;
// 索引签名
[prop: string]: string | number; // 值的类型是 string 或 number
}
const user: UserProfile = {
id: 123, // OK, number 是 string | number 的子类型
name: "Alice", // OK, string 是 string | number 的子类型
city: "New York", // OK, 'city' 是 string, "New York" 是 string
age: 30 // OK, 'age' 是 string, 30 是 number
};
interface InvalidProfile {
id: number; // Error: 类型"number"的属性"id"不能赋给"string"索引类型"string"
//索引签名要求所有值都是 string,但 id 是 number,不兼容!
[prop: string]: string; //
}
2.2 number
类型的键是 string
类型键的"特例"
因为 JavaScript 会将数字键转换为字符串键(例如 obj[5]
等同于 obj['5']
),TypeScript 也遵循这个规则。所以,如果你同时定义了 string
和 number
的索引签名,number
索引签名的值类型必须是 string
索引签名值类型的子类型。
typescript
interface DataCache {
[key: string]: any;
[index: number]: string; // OK, string 是 any 的子类型
}
interface InvalidDataCache {
[key: string]: string;
[index: number]: number; // Error! "number"索引类型"number"不能分配给"string"索引类型"string"
}
2.3 访问不存在的属性
typescript
interface ScoreRecord {
[name: string]: number;
}
const scores: ScoreRecord = { 'alice': 100 };
const bobScore = scores['bob']; // `bobScore` 的类型是 number,而不是 number | undefined
console.log(bobScore); // 输出:undefined
console.log(bobScore.toFixed(2)); // 运行时错误!TypeError: Cannot read properties of undefined (reading 'toFixed')
在 strictNullChecks
模式下,这会成为一个安全隐患。如何解决?
-
明确声明
undefined
:这是最推荐的方式。typescriptinterface ScoreRecord { [name: string]: number | undefined; } const scores: ScoreRecord = { 'alice': 100 }; const bobScore = scores['bob']; // `bobScore` 的类型现在是 number | undefined if (bobScore) { console.log(bobScore.toFixed(2)); // OK,类型被收窄 }
-
使用
noUncheckedIndexedAccess
:在tsconfig.json
中开启此选项,TypeScript 会自动在索引签名的结果中加入| undefined
。
typescript
interface ScoreRecord {
[name: string]: number;
}
const scores: ScoreRecord = { 'alice': 100 };
const bobScore = scores['bob']; // `bobScore` 的类型现在是 number | undefined
console.log(bobScore?.toFixed(2));// 在vscode中调用`toFixed()`时自动会补全`?`判断undefined
三、现代替代方案 Record<K, T>
Record<string, number>
和我们之前的 ScoreRecord
接口几乎是等价的:
typescript
// 使用 Record 工具类型
type ScoreRecord = Record<string, number>;
const scores: ScoreRecord = {
'alice': 100,
'bob': 95,
};
为什么推荐 Record
?
- 可读性更好 :
Record<string, number>
清晰地表达了"一个键为字符串、值为数字的记录"。 - 更灵活 :
Keys
参数不限于string
或number
,它可以是具体的字面量联合类型,从而创建更精确的对象类型。
typescript
type UserRole = 'admin' | 'user' | 'guest';
// 创建一个确保每种角色都存在的配置对象
const roleConfig: Record<UserRole, { permissions: string[] }> = {
admin: { permissions: ['create', 'read', 'update', 'delete'] },
user: { permissions: ['read', 'update'] },
//guest: { permissions: ['read'] }, //如果你漏掉了一个角色,TypeScript 会报错!
};
这是索引签名无法做到的。当你的键集合是已知且有限 的时,Record
或**映射类型(Mapped Types)**是比索引签名更好的选择。
总结
如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货