TypeScript 索引签名详解
目录
- 概述
- 基本语法
- 常见用法
- 高级特性
- 最佳实践
- 常见陷阱
概述
索引签名允许我们定义对象可以包含的属性的类型模式,而不需要明确列出所有属性。
基本语法
字符串索引签名
interface StringIndex {
[key: string]: string; // 基本字符串索引签名
}
const names: StringIndex = {
first: "John",
last: "Doe",
middle: "Smith"
};
数字索引签名
interface NumberIndex {
[index: number]: string; // 基本数字索引签名
}
const arr: NumberIndex = {
0: "First",
1: "Second",
2: "Third"
};
混合索引签名
interface MixedIndex {
[key: string]: any;
length: number; // 具体属性
name: string; // 具体属性
}
常见用法
1. 动态属性对象
interface Dictionary<T> {
[key: string]: T;
}
// 使用示例
const stringDict: Dictionary<string> = {
key1: "value1",
key2: "value2"
};
const numberDict: Dictionary<number> = {
count: 10,
age: 20
};
2. 类数组对象
interface ArrayLike {
[index: number]: string;
length: number;
}
const list: ArrayLike = {
0: "first",
1: "second",
length: 2
};
3. 映射类型
interface PersonMap {
[id: string]: {
name: string;
age: number;
}
}
const people: PersonMap = {
"123": { name: "John", age: 30 },
"456": { name: "Jane", age: 25 }
};
高级特性
1. 只读索引签名
interface ReadOnlyIndex {
readonly [key: string]: string;
}
const config: ReadOnlyIndex = {
api: "http://api.example.com",
token: "abc123"
};
// 错误:索引签名是只读的
config.api = "new-url"; // Error
2. 联合类型索引签名
interface FlexibleIndex {
[key: string]: string | number;
count: number;
name: string;
}
const obj: FlexibleIndex = {
count: 1,
name: "test",
extra: "additional",
value: 42
};
3. 条件类型与索引签名
type DynamicIndex<T> = {
[K in keyof T]: T[K] extends string ? string : number;
}
interface Person {
name: string;
age: number;
}
type PersonIndex = DynamicIndex<Person>;
// 结果:{ name: string; age: number; }
最佳实践
1. 使用泛型增加灵活性
interface GenericDict<T> {
[key: string]: T;
}
function createDict<T>(items: T[]): GenericDict<T> {
const dict: GenericDict<T> = {};
items.forEach((item, index) => {
dict[`item${index}`] = item;
});
return dict;
}
2. 类型安全的键值对
interface TypeSafeDict<K extends string | number | symbol, V> {
[key: K]: V;
}
// 使用字符串字面量类型
type ValidKeys = 'id' | 'name' | 'email';
interface UserDict {
[key in ValidKeys]: string;
}
3. 混合具体属性和索引签名
interface MixedProps {
id: number; // 具体属性
name: string; // 具体属性
[key: string]: any; // 额外的动态属性
}
const user: MixedProps = {
id: 1,
name: "John",
extraProp: true,
anotherProp: 42
};
常见陷阱
1. 数字索引与字符串索引的关系
interface NumberAndString {
[index: number]: string;
[key: string]: string | undefined; // 必须包含数字索引的返回类型
}
// 错误示例
interface Wrong {
[index: number]: string;
[key: string]: number; // Error: 数字索引类型必须是字符串索引类型的子类型
}
2. 属性检查
interface StringDict {
[key: string]: string;
}
const dict: StringDict = {
prop: "value"
};
// 运行时可以,但不推荐
const value = dict["nonexistent"]; // undefined
// 更安全的访问方式
const value2 = "prop" in dict ? dict["prop"] : undefined;
3. 索引签名与可选属性
interface OptionalProps {
[key: string]: string | undefined;
required: string; // OK
optional?: string; // OK
}
// 错误示例
interface WrongOptional {
[key: string]: string;
optional?: string; // Error: 可选属性必须匹配索引签名
}
实际应用示例
1. 缓存实现
interface Cache<T> {
[key: string]: {
value: T;
timestamp: number;
}
}
class DataCache<T> {
private cache: Cache<T> = {};
set(key: string, value: T): void {
this.cache[key] = {
value,
timestamp: Date.now()
};
}
get(key: string): T | undefined {
return this.cache[key]?.value;
}
}
2. 表单数据处理
interface FormData {
[fieldName: string]: string | string[] | undefined;
}
function processForm(data: FormData): void {
Object.entries(data).forEach(([field, value]) => {
if (Array.isArray(value)) {
console.log(`${field}: Multiple values`, value);
} else if (value) {
console.log(`${field}: Single value`, value);
}
});
}
3. API 响应处理
interface ApiResponse<T> {
data: T;
meta: {
[key: string]: unknown;
};
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
return response.json();
}
总结
- 索引签名的主要用途:
- 定义动态属性对象
- 实现字典或映射
- 处理未知属性集
- 最佳实践:
- 使用泛型增加类型安全
- 合理混合具体属性和索引签名
- 注意数字索引和字符串索引的关系
- 注意事项:
- 属性类型必须匹配索引签名
- 考虑使用 Map 或 Set 替代索引签名
- 注意可选属性与索引签名的兼容性