概述
- Record 类型是TS中其众多强大特性之一
- 它为我们提供了创建键值对映射的强大能力
- 极大地增强了代码的灵活性与类型安全性
应用示例
1 )用于配置场景
- 在复杂的项目中,配置文件往往包含多个模块的不同设置
- 使用 Record 可以确保配置的键名正确且值类型统一
示例
interface AppConfig {
port: number;
env: 'development' | 'production';
}
const config: Record<string, AppConfig> = {
server: { port: 3000, env: 'development' },
client: { port: 8080, env: 'production' },
};
2 )枚举场景
- Record 也常用于将枚举类型转换为易于使用的键值对映射,便于在代码中引用
示例
enum Color {
Red,
Green,
Blue,
}
const colorNameMap: Record<Color, string> = {
[Color.Red]: 'Red',
[Color.Green]: 'Green',
[Color.Blue]: 'Blue',
};
3 )联合类型场景
示例
type Scores = Record<'Math' | 'English' | 'Science', number>;
const studentScores: Scores = {
Math: 90,
English: 85,
Science: 92
};
4 )表单项的配置
- 在构建动态表单时,Record 可以用来定义表单项的配置,每个表单项都有其特有的属性
示例
interface FormField {
type: 'text' | 'checkbox' | 'select';
label: string;
required?: boolean;
}
const formConfig: Record<string, FormField> = {
username: { type: 'text', label: 'Username', required: true },
agreeTerms: { type: 'checkbox', label: 'Agree to terms', required: false },
};
5 ) 映射类型类似Record
type Invert<T> = {
[P in keyof T as T[P] extends string ? T[P] : never]: P;
};
type Original = { a: '1'; b: '2' };
type Inverted = Invert<Original>;
// 测试反转类型
const invertedObj: Inverted = {
'1': 'a',
'2': 'b'
};
// 这是一个类型守卫函数,用于在运行时检查对象是否符合 Inverted 类型
function isInvertedType(obj: any): obj is Inverted {
return '1' in obj && '2' in obj && obj['1'] === 'a' && obj['2'] === 'b';
}
if (isInvertedType(invertedObj)) {
console.log('invertedObj 符合 Inverted 类型');
} else {
console.log('invertedObj 不符合 Inverted 类型');
}
Invert<T>
类型别名通过条件映射- 使用了as关键字的映射类型语法,这在TypeScript 4.1及以后版本中可用
- 来生成一个新的类型,该类型实质上是原类型T中每个键值对的倒置
- 但这种方式更加灵活,因为它基于值的类型来进行映射
- 在这个例子中,要求原对象的值必须是字符串,从而可以用作新类型的键
- 因此,虽然Inverted类型在功能上类似于某种Record类型的应用
- 但它更准确地描述为一个经过类型系统转换处理后得到的、具有特定键值对的对象类型,而不是直接等同于使用Record定义的类型
6 )自定义Record(硬编码联合类型映射)
示例
// 1. 使用 TypeScript 内置的 Record 类型
type UserRecord = Record<string, any>; // 允许任何字符串作为键,任何类型作为值
// 假设我们有一个具体的键集合和值类型
type SpecificKeys = 'username' | 'age';
type SpecificUserRecord = Record<SpecificKeys, string>; // 键必须是 'username' 或 'age',值必须是 string
// 2. 自定义一个类似 Record 的类型,但键是硬编码的(这不是一个好的做法,因为它不够灵活)
type MyRecord<T> = {
[P in 'username' | 'age']: T; // 硬编码的键 'username' 和 'age',值类型由泛型 T 指定
};
// 使用自定义的 MyRecord 类型
type MyUserRecord = MyRecord<string>; // 键是 'username' 或 'age',值都是 string
// 示例对象
const user: SpecificUserRecord = {
username: 'Alice',
age: '30' // 注意:这里为了演示使用了字符串,但在实际应用中年龄应该是数字
};
const myUser: MyUserRecord = {
username: 'Bob',
age: '25' // 同样,这里使用了字符串作为示例
};
// 说明:
// 1. 内置的 Record 类型更加灵活,允许您指定任何键的集合(K extends keyof any)和值的类型(T)。
// 2. 自定义的 MyRecord 类型在这里是硬编码的,它只允许 'username' 和 'age' 作为键,并且值的类型由泛型 T 指定。
// 3. 在实际应用中,通常推荐使用内置的 Record 类型,因为它更加灵活且易于维护。
7 ) 类型谓词与模式匹配
示例
function isRecordOfStrings(obj: any): obj is Record<string, string> {
return typeof obj === 'object' && obj !== null && Object.values(obj).every(v => typeof v === 'string');
}
if (isRecordOfStrings(someObj)) {
// 在此块内,someObj 被认为是 Record<string, string>
}
- 通过类型谓词和 Record 结合,可以实现更加精准的类型判断和模式匹配逻辑
8 ) Record 与 索引签名
示例
// 假设我们有一个Goods类型
type Goods = {
id: number;
name: string;
};
// TypeScript内置的Record类型
type RecordType<K extends keyof any, T> = {
[P in K]: T;
};
// 使用Record类型来创建一个新的类型,该类型的属性与Goods的键相同,但值都是string类型
type resultGoodsType = RecordType<keyof Goods, string>;
// 这等价于:
// type resultGoodsType = {
// id: string;
// name: string;
// };
// 类似索引签名的类型定义(但不是Record)
type IndexSignatureType = {
[x: string]: any; // 字符串索引签名,允许任何字符串作为键,值是any类型
// 注意:一旦你定义了字符串索引签名,那么就不能再为特定的属性名定义更具体的类型了
// 因为所有属性名最终都会被视为字符串,并遵循这个索引签名的类型
};
// 索引签名不能包含number或symbol作为键类型(除了特殊的数组和Map类型)
// type BadIndexSignatureType = {
// [x: number]: any; // 错误:索引签名参数类型必须为 "string" 或 "number" 或 "symbol"。
// };
// 使用Record和索引签名的区别
// Record类型允许你明确指定键的类型和值的类型
// 而索引签名则提供了一种更通用的方式来描述对象的结构,但会限制你对特定键的类型定义
// 示例:使用Record和索引签名
const recordExample: resultGoodsType = {
id: '123', // 符合resultGoodsType的定义,因为id现在是string类型
name: 'Apple' // 符合resultGoodsType的定义,因为name现在是string类型
};
const indexSignatureExample: IndexSignatureType = {
id: 123, // 符合IndexSignatureType的定义,因为id被视为字符串键,但其值可以是any类型
name: 'Banana', // 符合IndexSignatureType的定义
'some-other-key': true // 也符合,因为所有字符串键都符合索引签名的定义
};
联系:
- Record类型和索引签名都用于描述对象的结构
- 它们都允许你使用类型参数来定义键和值的类型
区别:
- Record是一个内置工具类型,它接受两个类型参数来明确指定键和值的类型
- 索引签名是TypeScript中对象类型定义的一部分,它允许你定义一个或多个类型的键(通常是string或number),并为这些键指定一个值的类型
- 一旦你定义了一个字符串索引签名,那么你就不能再为对象的特定属性定义更具体的类型了(除非该类型与索引签名的类型兼容)
- 这是因为所有属性名最终都会被视为字符串,并遵循这个索引签名的类型。
- Record类型提供了一种更精确和可控的方式来定义对象的结构,而索引签名则提供了一种更通用和灵活的方式来描述对象的结构
9 )实现轻量级 Map
type SimpleMap<KeyType extends string | number | symbol, ValueType> = {
[key in KeyType]: ValueType;
};
class LightMap<KeyType extends string | number | symbol, ValueType> {
private map: SimpleMap<KeyType, ValueType>;
constructor() {
this.map = {} as SimpleMap<KeyType, ValueType>;
}
set(key: KeyType, value: ValueType): void {
this.map[key] = value;
}
get(key: KeyType): ValueType | undefined {
return this.map[key];
}
delete(key: KeyType): boolean {
const hasKey = key in this.map;
if (hasKey) {
delete this.map[key];
}
return hasKey;
}
has(key: KeyType): boolean {
return key in this.map;
}
}
const myMap = new LightMap<string, number>();
myMap.set('apple', 1);
myMap.set('banana', 2);
console.log(myMap.get('apple')); // 输出:1
console.log(myMap.has('cherry')); // 输出:false
console.log(myMap.delete('banana')); // 输出:true
console.log(myMap.has('banana')); // 输出:false
- Record是TypeScript的一个内置类型别名,它允许你基于一个索引类型(键)和一个值类型创建一个对象类型
- 例如,Record<string, number>定义了一个所有键为字符串、值为数字的对象类型
- 我们的轻量级Map实现将包括以下几个基本操作:
- 设置值:
set(key, value)
- 获取值:
get(key)
- 删除值:
delete(key)
- 检查找键是否存在:
has(key)
- 设置值:
- 泛型SimpleMap,允许键为string、number或symbol,值为任意类型ValueType
- 通过TypeScript的Record类型和类,我们实现了一个轻量级的Map数据结构,它在编译时提供类型安全检查,确保了键值的类型正确性
- 虽然它不具备JavaScript内置Map对象的所有功能(如迭代器、容量自动扩展等),但足以应对一些简单场景,且在类型安全方面提供了额外保障