介绍
结构化类型是typescript类型系统的一个重要特性,如果不了解这个特性,则经常会被typescript的行为搞得一头雾水,导致我们期待的行为与实际的行为不一致。今天我们就来看两个例子。
不了解结构化类型的同学,可以先看看这篇:// 插入文章链接。
第一个例子
下面的代码定义了一个Person
类型
typescript
interface Person {
name: string;
age: number;
}
然后又定义了一个函数打印这个类型的对象
typescript
function printPerson(person: Person) {
console.log(`Name: ${person.name}, Age: ${person.age}`);
}
按道理来说,要调用这个函数,必须传递一个Person
类型的对象,但是你会发现,直接传一个对象进去也行。
typescript
printPerson({ name: "Alice", age: 30 });
这段代码没有报错,为什么呢?因为typescript的结构化类型系统认为,只要传入的对象包含了Person
类型所需的所有属性,就可以被认为是Person
类型。你甚至可以多加一些属性,比如:
typescript
printPerson({ name: "Alice", age: 30, location: "Wonderland" });
代码一样可以正常运行!
为什么?因为在typescript中,类型是基于结构
的,而不是基于名称的。只要对象的结构符合要求,就可以被认为是该类型。如果一个类型A包含了类型B的所有属性,那么类型A就可以被认为是类型B。在使用类型B的地方,就可以使用类型A代替。
第二个例子
还是以上面的Person
类型为例,假设我们要打印Person
对象中的所有属性,有的同学可能不假思索的写下如下代码:
typescript
interface Person {
name: string;
age: number;
}
const person: Person = { name: "Alice", age: 30 };
function printProperties(person: Person) {
for (const property in person) {
console.log(`${property}: ${person[property]}`);
}
}
printProperties(person);
但是这段代码却报错了:
bash
TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Person'. No index signature with a parameter of type 'string' was found on type 'Person'.
当我第一次看到这个错误时,我只想撞墙,我哪里用any
了,这不是胡扯吗?但这不是对待错误的正确态度,这种错误如果不彻底解决,那么它就会一直困扰你,只有将它彻底击败,下次再遇到时才能得心应手!
仔细看一下这个报错,它大概描述了两件事情:
string
类型的值不能用来索引Person
类型。Person
类型没有定义索引签名。
其实这两件事本质上说的是一个问题,那就是在TypeScript中,只有在类型中显式定义了索引签名,才能使用string
类型的值来索引该类型。那么我们就给Person
类型添加一个索引签名:
方式一:为Person
类型添加索引签名
typescript
interface Person {
name: string;
age: number;
[key: string]: any; // 索引签名
}
[key: string]: any;
这行代码的意思是,Person
类型可以有任意数量的属性,属性名必须是字符串类型 ([key: string]
),属性值可以是任意类型(any
)。
现在我们再来运行printProperties
函数,就不会报错了。
方式二:使用keyof
关键字
坦白的说,为了一个遍历函数给Person
类型添加一个索引签名有点过于冗余了,其实我们可以使用另一个方法来解决这个问题,那就是使用keyof
关键字来获取Person
类型的所有属性名。
typescript
function printProperties(person: Person) {
for (const property in person) {
console.log(`${property}: ${person[property as keyof typeof person]}`);
}
}
来看这一句代码property as keyof typeof person
, 它的执行步骤是这样的:
- 先执行
typeof person
,得到Person
类型。 - 再执行
keyof Person
,得到Person
类型的所有属性名的联合类型 -name | age
。 - 最后使用
as
操作符将property
转换为这个联合类型。
这样做的好处是,property
的类型被限制为Person
类型的属性名,在本例中就是name
和age
这两个属性,绝不会超出这个范围,这样就可以安全地索引person
对象了。
眼力好的同学可能已经发现了,上面这个写法可以简化一下,property as keyof typeof person
可以简化为property as keyof Person
,因为person
的类型就是Person
,所以我们可以直接使用Person
类型来代替。这样可以节省一个typeof
操作符的使用。
方式三:使用Object.entries
当然,我们还可以使用Object.entries
方法来遍历对象的属性,这样就不需要担心索引签名的问题了。
typescript
function printProperty(person: Person) {
Object.entries(person).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
}
分析一下这段代码:
Object.entries
方法会返回一个二维数组,其中每个元素又是一个数组,这个数组包含了对象的属性名和属性值。以上面的person
对象为例,Object.entries(person)
会返回[['name', 'Alice'], ['age', 30]]
,- 接下来的
forEach
方法会遍历这个数组,这里使用了一个数组解构操作符([key, value])
,将每个属性的名字赋值给key,属性的值赋值给value, - 最后使用
console.log
打印出来。
我比较喜欢方式三,简洁易懂,无需额外的操作。
今天就到这里了,觉得有用就点个关注吧,我们下次再见,我要去打弹弓了。