说说TS中的Record
Record
是 ts 中的一个高级类型,它可以用来构造一个具有给定类型T
的一组属性K
的类型。它的定义如下:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
这里,K
是一个可以被赋值给keyof any
的类型,也就是说,它可以是一个字符串、数字、符号或者联合类型。T
是任意类型。[P in K]: T
表示将K
中的每个属性名映射到类型T
上,从而得到一个新的类型。
我们来看几个栗子 🌰:
// 将一个枚举类型的值映射到另一个类型上
enum Color {
Red,
Green,
Blue,
}
type ColorInfo = Record<Color, { name: string; hex: string }>;
const colors: ColorInfo = {
[Color.Red]: { name: 'red', hex: '#ff0000' },
[Color.Green]: { name: 'green', hex: '#00ff00' },
[Color.Blue]: { name: 'blue', hex: '#0000ff' },
};
在上述示例中,使用了枚举类型Color
作为Record
的第一个参数,表示创建一个以颜色值为属性名的类型。然后使用了一个对象类型作为第二个参数,表示每个颜色都有一个名字和一个十六进制值。最后,指定变量 colors
的类型为 ColorInfo
,即可让 colors
应用 ColorInfo
类型约束。
// 将一个联合类型的值映射到另一个类型上
type Animal = 'dog' | 'cat' | 'fish';
type AnimalInfo = Record<Animal, { name: string; age: number }>;
const animals: AnimalInfo = {
dog: { name: 'dogName', age: 2 },
cat: { name: 'catName', age: 3 },
fish: { name: 'fishName', age: 5 },
};
在上述示例中,将联合类型Animal
作为第一个参数,表示创建一个以动物名为属性名的类型。然后将一个对象类型作为第二个参数,表示每个动物都有一个名字和一个年龄。
// 将一个对象类型的属性映射到另一个类型上
interface Person {
id: number;
name: string;
}
type PersonInfo = Record<keyof Person, string>;
const person: PersonInfo = {
id: '001',
name: 'Alice',
};
在上面的这个例子中,使用对象类型Person
的键集合作为第一个参数,表示创建一个与Person
具有相同属性名的类型。然后使用字符串类型作为第二个参数,表示每个属性都是字符串类型。
可以看到,使用Record
可以方便地将一个类型的属性映射到另一个类型上,并创建出新的类型。这在一些场景下非常有用,比如:
- 在定义一些常量或配置时,可以使用枚举或联合类型作为键,并将对应的值映射到一个对象类型上,保证键的唯一性和值的结构性。
- 当封装一些通用的函数或方法时,可以使用
Record
来定义参数或返回值的类型,以提高代码的复用性和可读性。 - 对一个已有的类型进行扩展或修改时,可以使用
Record
来覆盖或替换原有的属性类型,从而避免创建新的类型或使用交叉类型。
嗯,Record
确实是一个非常强大和实用的工具类型,帮助我们在 ts 中进行更灵活和高效的类型操作。但是,它也有一些局限性,比如:
- 只能将一个类型的属性映射到另一个类型上,不能根据属性名或属性值进行条件判断或变换。
- 不能保留原有类型的修饰符,比如
readonly
或optional
。 - 不能处理动态的或未知的属性名。
举个例子,假设我们有一个类型 User
,它有三个属性 id
、name
和 email
,其中 id
是只读的,name
是必选的,email
是可选的,我们想要使用 Record
来创建一个以用户 id
为键,用户信息为值的类型,如下:
interface User {
readonly id: number;
name: string;
email?: string;
}
type UserMap = Record<number, User>;
但是这样做有一个问题,就是 Record
会忽略原有类型的修饰符,它会将所有的属性都变成可读写和必选的,这可能不是我们想要的结果。我们可以使用内置的工具类型来解决这个问题,比如:
interface User {
readonly id: number;
name: string;
email?: string;
}
type UserMap = Record<number, Readonly<Partial<User>>>;
在这个例子中,我们使用了 Readonly
和 Partial
来保留原有类型的修饰符,它会将所有的属性都变成只读和可选的,这样就符合我们的预期了。当然,这里我们也可以根据实际情况来调整修饰符的使用,比如:
interface User {
readonly id: number;
name: string;
email?: string;
}
type UserMap = Record<number, Partial<Pick<User, 'name' | 'email'>>>;
在上述示例中,我们使用了 Partial
和 Pick
来保留原有类型的部分属性和修饰符,使用 Partial
将所有的属性都变成可选的,使用 Pick
来指定只包含 name
和 email
两个属性,这样就可以减少数据冗余了。
与 object 和索引签名的区别
从上面的介绍中可以看出,Record
是用来声明一个对象的结构类型的。在 ts 中,object
和索引签名也可以做类似的事情,那么它们之间有什么区别?
object
是 ts 中的基础类型,它可以用来表示任意的 js 对象,比如:
const obj: object = {
name: 'Bob',
age: 30,
};
// 但是使用object类型声明的对象,不能访问其属性
// 因为object类型并不知道对象的结构,所以不能访问其属性
obj.name; // error
obj.age; // error
obj.gender; // error
// 如果要访问对象的属性,需要使用类型断言
(obj as { name: string }).name; // ok
(obj as { age: number }).age; // ok
而索引签名用来表示对象的结构类型,比如:
interface User {
id: number;
name: string;
email: string;
}
const user: User = {
id: 1,
name: 'Alice',
email: '',
};
// 因为索引签名知道对象的结构,所以可以访问其属性
user.id; // ok
user.name; // ok
user.email; // ok
既然如此,Record
和索引签名都是用来声明对象的结构类型的,那我直接都用索引签名一把梭不行吗?比如下面的示例,我用索引签名声明:
// 用Record声明
type UserMap = Record<number, User>;
// 用索引签名达到相同的效果
type UserMap1 = { [id: number]: User };
确实,这两种方式都可以用来声明一个对象的结构类型,但是它们之间还是有一些区别的,比如:
Record
可以使用泛型,而object
和索引签名不行。Record
可以使用内置的工具类型,而object
和索引签名不行。Record
可以使用联合类型作为键,而object
和索引签名不行。- 等等等等…(还有很多,不一一列举了)。
文字是苍白的,来看一个例子:
type Color = 'red' | 'green' | 'blue';
// 使用 Record 定义类型
type ColorMap1 = Record<Color, number>;
// 使用 object 定义类型
type ColorMap2 = {
[key in Color]: number;
};
// 使用索引签名定义类型
interface ColorMap3 {
[key: string]: number;
}
可以看到,使用 Record
的方式是最简洁的,因为它可以一次性定义键和值的类型,并且使用对象字面量语法来创建对象。而使用 object
的方式虽然也可以一次性定义键和值的类型,但需要使用映射类型语法来定义类型,使用起来略微麻烦。使用索引签名定义类型则最为灵活,可以使用任意类型作为键的类型,但需要手动定义键和值的类型,并使用对象字面量语法来创建对象。
另外,如果你需要使用泛型的话,Record
也是唯一的选择,例如:
// 使用 Record 定义类型
type RecordWithGeneric<K extends string, T> = Record<K, T>;
const record1: RecordWithGeneric<'a' | 'b', number> = {
a: 1,
b: 2,
};
// 使用索引签名定义类型
interface IndexSignatureWithGeneric<K extends string, T> {
[key: K]: T; // 错误:索引签名不能使用泛型类型参数 K
}
const indexSignature1: IndexSignatureWithGeneric<'a' | 'b', number> = {
a: 1,
b: 2,
};
上面的这里例子比较简单,也说明了它们之间能力的一些区别,如果更加复杂一点,我们还可以实现一些使用 object
或索引签名无法实现的功能。
// 假设有一个类型Student,它有三个属性id、name和score
interface Student {
id: number;
name: string;
score: number;
}
// 想要定义一个类型StudentMap,它是一个以学生id为键,学生信息为值的对象
// 那么,我们可以使用Record来实现这个需求,如下:
type StudentMap = Record<number, Student>;
// 创建一个变量students,它的类型是StudentMap,并且可以访问它的任何属性
const students: StudentMap = {
1: { id: 1, name: 'Alice', score: 90 },
2: { id: 2, name: 'Bob', score: 80 },
};
students[1].name; // ok
students[2].score; // ok
// 如果使用object或索引签名来定义StudentMap,就会有一些问题,比如:
type StudentMap2 = object;
const students2: StudentMap2 = {
1: { id: 1, name: 'Alice', score: 90 },
2: { id: 2, name: 'Bob', score: 80 },
};
students2[1].name; // error
students2[2].score; // error
// 因为object类型不知道对象具有哪些属性或属性的类型,所以不能访问任何属性
type StudentMap3 = { [index: number]: any };
const students3: StudentMap3 = {
1: { id: 1, name: 'Alice', score: 90 },
2: { id: 2, name: 'Bob', score: 80 },
};
students3[1].name; // ok
students3[2].score; // ok
// 这是因为索引签名类型假设对象的属性值都是any类型,所以可以访问任何属性,但是这样会失去类型安全性
type StudentMap4 = { [index: number]: Student };
const students4: StudentMap4 = {
1: { id: 1, name: 'Alice', score: 90 },
2: { id: 2, name: 'Bob', score: 80 },
};
students4[1].name; // ok
students4[2].score; // ok
// 这里索引签名类型指定了对象的属性值都是Student类型,所以可以访问Student的属性,但是这样会限制对象的属性名只能是数字
可以看到,使用 Record
可以更灵活和高效地定义对象类型,而使用 object
或索引签名则有一些局限性或缺点。
所以,当我们需要声明一个对象的结构类型时,最好还是使用 Record
,这样可以获得更多的灵活性和可扩展性。
总结
Record
是一个非常实用的工具类型,它可以帮助我们在 ts 中进行更灵活和高效的类型操作。但是,它也有一些局限性,比如不能根据属性名或属性值进行条件判断或变换,不能保留原有类型的修饰符,不能处理动态的或未知的属性名等。合理的使用 Record
可以提高代码的复用性和可读性,但是过度的使用 Record
也会导致代码的可维护性变差,所以在使用 Record
时,一定要根据实际情况来选择合适的使用方式。
- 本博客所有文章除特别声明外,均可转载和分享,转载请注明出处!
- 本文地址:https://www.leevii.com/?p=3242