在使用 Typescript 编写代码时,有时候我们会遇到只有运行时才能确定类型的情况,在这种情况下,typescript 的联合类型会给我们带来一些麻烦。
怪物的眼睛在哪里?
假设我们遇到了一种生物,它有两种状态————盛年期和幼年期,幼年期时它的眼睛长在脸上,盛年期时眼睛长在胸口。
现在我们要编写一个函数,它接受这种生物为参数,并找到它的眼睛在哪里:
const littleMonster = {
face: {
eyes: "littleMonster-eyes"
}
}
const bigMonster = {
chest: {
eyes: "bigMonster-eyes"
}
}
function whereEyes(monster) {
return monster.face || monster.chest
}
const eyes1 = whereEyes(littleMonster)
const eyes2 = whereEyes(bigMonster)
console.log(eyes1); // { eyes: 'littleMonster-eyes' }
console.log(eyes2); // { eyes: 'bigMonster-eyes' }
接下来我们给它加上 ts 类型:
type eyesType = {
eyes: string
}
type littleMonsterType = {
face: eyesType
}
type bigMonsterType = {
chest: eyesType
}
const littleMonster: littleMonsterType = {
face: {
eyes: "littleMonster-eyes"
}
}
const bigMonster: bigMonsterType = {
chest: {
eyes: "bigMonster-eyes"
}
}
function whereEyes(monster: littleMonsterType | bigMonsterType) : eyesType {
return monster.face || monster.chest
}
const eyes1 = whereEyes(littleMonster)
const eyes2 = whereEyes(bigMonster)
console.log(eyes1); // { eyes: 'littleMonster-eyes' }
console.log(eyes2); // { eyes: 'bigMonster-eyes' }
看上去似乎没有什么问题,但编译器可不是这么认为的,我们可以在 whereEyes 函数中看到两条红色波浪线:
function whereEyes(monster: littleMonsterType | bigMonsterType) : eyesType {
return monster.face || monster.chest
}
类型“littleMonsterType | bigMonsterType”上不存在属性“face”。
类型“bigMonsterType”上不存在属性“face”。ts(2339)
类型“littleMonsterType | bigMonsterType”上不存在属性“chest”。
类型“littleMonsterType”上不存在属性“chest”。ts(2339)
| 表示联合类型,即 monster 可以接受 littleMonsterType 或 bigMonsterType 作为参数,如果我们传入一个 bigMonsterType 类型的 monster ,显然它上面并没有 face 属性,这时我们试图访问 monster.face 当然会被编译器阻止。 访问 monster.chest 也是相同的道理。
那么我们接下来介绍几种办法来编写这种场景下的 ts :
类型保护与断言
一种自然的想法是:我们可以先用一个函数判断一下怪物的眼睛在哪里,然后再去查找它:
function isLittleMonster(arg: littleMonsterType | bigMonsterType): arg is littleMonsterType {
return (<littleMonsterType>arg).face !== undefined;
}
function whereEyes(monster: littleMonsterType | bigMonsterType): eyesType {
if (isLittleMonster(monster)) {
return monster.face
} else {
return monster.chest
}
}
这里的 is 语法来自 类型保护 概念。
我们让函数返回一个 is 语句,它是一个布尔值。直观的来看,如果为 arg is littleMonsterType == true ,则这个变量 arg/monster 就会被编译器视为为 littleMonsterType 类型。
我们再来看看函数内的 (<littleMonsterType>arg) :我们将 arg 断言为 littleMonsterType 并访问其 face 属性,如果访问到了,则可以确定这个变量为 littleMonsterType .
总结一下上面我们做的事情:因为我们无法判断 monster 的类型,所以我们可以
- 断言
monster的类型,进行一次类型检查 - 用检查的结果来确定
monster的真实类型。
类与 instanceof
instanceof 可以帮我们比较一个实例是否是它的原型类派生的,于是我们有了另一个判断 monster 类型的方法,但我们需要定义两个原型类,再实例化出 littleMonster 和 bigMonster 对象:
type eyesType = {
eyes: string
}
interface littleMonsterType {
face: eyesType
}
interface bigMonsterType {
chest: eyesType
}
class LittleMonster implements littleMonsterType {
public face: {
eyes: string
}
constructor() {
this.face = {
eyes: "littleMonster-eyes"
}
}
}
class BigMonster implements bigMonsterType {
public chest: {
eyes: string
}
constructor() {
this.chest = {
eyes: "bigMonster-eyes"
}
}
}
const littleMonster = new LittleMonster()
const bigMonster = new BigMonster()
ts 中定义类的语法与 Java 等静态语言类型类似,先声明属性的 scope ———— public/private ,然后在 construct 中给已声明的属性赋值。这里声明属性公有,因此我们可以直接通过实例的 monster.face 或 monster.chest 来访问被相应的类实例化后的属性值。
现在我们可以通过 instanceof 来判断 monster 是哪个怪物类的实例了:
function whereEyes(monster: littleMonsterType | bigMonsterType): eyesType | undefined {
if (monster instanceof LittleMonster) {
return monster.face
}
if (monster instanceof BigMonster) {
return monster.chest
}
}
const eyes1 = whereEyes(littleMonster)
const eyes2 = whereEyes(bigMonster)
console.log(eyes1); // { eyes: 'littleMonster-eyes' }
console.log(eyes2); // { eyes: 'bigMonster-eyes' }