索引签名与动态属性
00:00
索引签名、Record与动态属性访问
概述
索引签名(Index Signature)是 TypeScript 中定义动态属性的核心机制,允许对象拥有任意数量的属性,只要这些属性的键和值符合指定的类型。Record 工具类型是索引签名的常用替代方案,提供了更精确的键约束。动态属性访问通过 keyof 和泛型约束实现类型安全。理解索引签名、Record 和动态属性访问的使用场景和限制,对于处理配置对象、字典映射和动态数据结构至关重要。
基础概念
索引签名:在接口或类型别名中使用 [key: Type]: ValueType 语法定义动态属性。键类型只能是 string、number 或 symbol。
Record 工具类型:Record<K, V> 创建键类型为 K、值类型为 V 的对象类型。比索引签名更精确,可以限制键为特定的联合类型。
动态属性访问:使用 obj[key] 语法访问动态属性。TypeScript 通过 keyof 和泛型约束确保键的类型安全。
索引签名与已知属性:索引签名与已知属性共存时,已知属性的类型必须是索引签名值类型的子类型。
快速上手
索引签名
// 字符串键的索引签名
interface StringMap {
[key: string]: string;
}
const dict: StringMap = { name: '张三', city: '北京' };
// 数字键的索引签名
interface NumberMap {
[key: number]: string;
}
const list: NumberMap = { 0: '第一项', 1: '第二项' };
// 混合类型值
interface Config {
[key: string]: string | number | boolean;
name: string; // 已知属性,必须是索引签名值类型的子类型
version: number;
}
const appConfig: Config = { name: '应用', version: 1, debug: true };
Record 工具类型
// Record 限制键为特定联合类型
type UserRoles = Record<'admin' | 'editor' | 'viewer', boolean>;
const roles: UserRoles = { admin: true, editor: false, viewer: true };
// Record 与索引签名的区别
// 索引签名:键可以是任意 string
interface LooseMap {
[key: string]: string;
}
const loose: LooseMap = { any: 'value' }; // 允许任意键
// Record:键必须是指定的联合类型
type StrictMap = Record<'a' | 'b', string>;
const strict: StrictMap = { a: '1', b: '2' }; // 必须包含 a 和 b
动态属性访问
// 类型安全的属性访问
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: '张三', age: 30, email: 'test@example.com' };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'phone'); // 错误:'phone' 不是 user 的键
// 动态设置属性
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
setProperty(user, 'name', '李四'); // 正确
// setProperty(user, 'name', 123); // 错误:name 应为 string
详细用法
索引签名与已知属性共存
// 已知属性的类型必须兼容索引签名
interface ApiResponse {
[key: string]: string | number | boolean | object;
status: number; // number 是 string | number | boolean | object 的子类型
message: string; // string 是 string | number | boolean | object 的子类型
data: object; // object 是 string | number | boolean | object 的子类型
}
// 错误示例:已知属性类型不兼容索引签名
// interface BadExample {
// [key: string]: string;
// count: number; // 错误:number 不能赋值给 string
// }
// 解决方案:使用联合类型
interface FixedExample {
[key: string]: string | number;
count: number;
label: string;
}
模板字面量键
// 使用模板字面量类型约束键
type CSSProperties = {
[K in `--${string}`]?: string;
};
const styles: CSSProperties = {
'--primary-color': '#2196F3',
'--font-size': '16px',
};
// 事件处理器映射
type EventKey = `on${Capitalize<string>}`;
type EventMap = {
[K in EventKey]?: (...args: any[]) => void;
};
const handlers: EventMap = {
onClick: () => console.log('clicked'),
onChange: () => console.log('changed'),
};
嵌套索引签名
// 嵌套字典结构
interface NestedDict {
[key: string]: {
[key: string]: number;
};
}
const scores: NestedDict = {
math: { midterm: 85, final: 90 },
english: { midterm: 78, final: 82 },
};
// 安全访问嵌套属性
function getNestedValue(obj: NestedDict, key1: string, key2: string): number | undefined {
return obj[key1]?.[key2];
}
条件索引签名
// 根据条件选择不同的索引签名类型
type Dictionary<T, V> = T extends string ? Record<T, V> : { [key: string]: V };
type StringKeyDict = Dictionary<'a' | 'b', number>;
// Record<'a' | 'b', number> -> { a: number; b: number }
type AnyKeyDict = Dictionary<string, number>;
// { [key: string]: number }
常见场景
配置对象
// 应用配置:允许任意键,但值类型有限制
interface AppConfig {
[key: string]: string | number | boolean | object;
appName: string;
version: number;
}
const config: AppConfig = {
appName: '我的应用',
version: 1,
debug: true,
api: { baseURL: 'https://api.example.com', timeout: 5000 },
};
// 读取配置
function getConfig<T extends string | number | boolean>(key: string, defaultValue: T): T {
const value = config[key];
if (typeof value === typeof defaultValue) {
return value as T;
}
return defaultValue;
}
缓存映射
// 类型安全的缓存
class TypedCache<T> {
private cache: Record<string, T> = {};
set(key: string, value: T): void {
this.cache[key] = value;
}
get(key: string): T | undefined {
return this.cache[key];
}
has(key: string): boolean {
return key in this.cache;
}
delete(key: string): boolean {
return delete this.cache[key];
}
keys(): string[] {
return Object.keys(this.cache);
}
values(): T[] {
return Object.values(this.cache);
}
}
// 使用
const userCache = new TypedCache<User>();
userCache.set('user:1', { id: 1, name: '张三' });
const user = userCache.get('user:1'); // User | undefined
国际化字典
// 国际化翻译字典
type Locale = 'zh-CN' | 'en-US';
type TranslationDict = Record<string, string>;
const translations: Record<Locale, TranslationDict> = {
'zh-CN': {
'app.title': '我的应用',
'user.greeting': '你好',
'button.submit': '提交',
},
'en-US': {
'app.title': 'My App',
'user.greeting': 'Hello',
'button.submit': 'Submit',
},
};
function t(locale: Locale, key: string): string {
return translations[locale][key] ?? key;
}
注意事项
- 索引签名的类型限制:索引签名的键类型只能是 string、number 或 symbol。不能使用其他类型(如联合类型)作为键类型。
- 数字键与字符串键:JavaScript 中对象的键总是字符串。数字键会被自动转换为字符串。TypeScript 区分数字索引签名和字符串索引签名,但运行时行为一致。
- 索引签名与 Object.keys:
Object.keys()返回string[],而不是keyof T[]。遍历对象键时需要类型断言或类型守卫。 - Record 的完整性检查:Record 要求所有指定的键都存在。如果某些键是可选的,应使用
Partial<Record<K, V>>。 - satisfies 替代索引签名:当需要验证对象结构但保留具体类型时,使用
satisfies比索引签名更合适。
进阶用法
动态代理对象
// 使用 Proxy 创建类型安全的动态对象
function createTypedProxy<T extends Record<string, any>>(handler: (key: string) => T[string]): T {
return new Proxy({} as T, {
get(_, key: string) {
return handler(key);
},
});
}
// 创建环境变量代理
const env = createTypedProxy<string>((key) => process.env[key] ?? '');
env.VITE_API_URL; // string
env.VITE_DEBUG; // string
递归索引类型
// 递归嵌套对象
type DeepRecord<T> = {
[key: string]: T | DeepRecord<T>;
};
// 使用
const deepConfig: DeepRecord<string> = {
api: {
baseURL: 'https://api.example.com',
auth: {
clientId: 'app-123',
clientSecret: 'secret',
},
},
features: {
darkMode: 'true',
},
};
// 安全访问
function getDeepValue(obj: DeepRecord<string>, path: string): string | undefined {
const keys = path.split('.');
let current: DeepRecord<string> | string = obj;
for (const key of keys) {
if (typeof current === 'string') return undefined;
current = current[key];
if (current === undefined) return undefined;
}
return typeof current === 'string' ? current : undefined;
}