Typescript联合类型的“坑”

58 阅读3分钟

在使用 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 可以接受 littleMonsterTypebigMonsterType 作为参数,如果我们传入一个 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 的类型,所以我们可以

  1. 断言 monster 的类型,进行一次类型检查
  2. 用检查的结果来确定 monster 的真实类型。

类与 instanceof

instanceof 可以帮我们比较一个实例是否是它的原型类派生的,于是我们有了另一个判断 monster 类型的方法,但我们需要定义两个原型类,再实例化出 littleMonsterbigMonster 对象:

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.facemonster.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' }