缩小类型而不必声明特定类型保护函数?



比如说,我有一个这样的类,TypeScript不喜欢它:

class Foo extends ThirdPartyLibrary {
animal!: Animal;
bar(): void {
this.animal.quack(); // Method `quack` does not exist on type `Animal` 😱
}
}
解决这个问题的官方方法似乎是使用一个特别的类型缩小函数:
class Foo extends ThirdPartyLibrary {
animal!: Animal;
bar(): void {
function isDuck(maybeDuck: unknown): maybeDuck is Duck {
return maybeDuck?.look === 'ducky' && maybeDuck?.voice === 'quacky';
}
if (!isDuck(this.animal)) {
throw new Error('Expected argument to be a duck');
}
this.animal.quack();
}
}

在某些情况下,这样的运行时检查是多余的。例如,当使用一个类型很差的第三方库时,我不想在运行时测试库并编写所有的样板代码。

相反,我想把它写得尽可能短。

到目前为止,我最好的尝试是使用as类型强制转换将变量重新分配给另一个变量:
class Foo extends ThirdPartyLibrary {
animal!: Animal;
bar(): void {
const animal = this.animal as unknown as Duck;
animal.quack(); // Now TypeScript knows that this animal can quack
}
}

但是我不需要这个变量!我只需要TypeScript静态地知道类型,我不想声明任何运行时变量或if子句或抛出错误。

我想象的是这样的:

class Foo extends ThirdPartyLibrary {
animal!: Animal;
bar(): void {
this.animal is Duck; // Narrow down the type without boilerplate
this.animal.quack();
}
}

我只是想让TypeScript静态地知道,在代码的这一点上,这个变量/属性肯定是一个特定的类型。我怎么做呢?


不适合我的解决方案:

  1. 输入animal属性为Duck:

    class Foo extends ThirdPartyLibrary {
    animal!: Duck;
    }
    

    这个解决方案对于我的简单例子来说是最合理的,但那只是我想出的一个最小的人工例子。

    想象Foo类比这更复杂:this.anmial被使用了很多次,大多数时候它可以是任何Animal。只有在代码的这个特定点,它才被确定为鸭子。

  2. 使用内嵌类型转换:(this.animal as unknown as Duck).quack().

    这个方法有效,但是当你需要对这只动物做不止一件事情时,这种方法就变得很烦人了:

    (this.animal as unknown as Duck).quack();
    (this.animal as unknown as Duck).waddle();
    (this.animal as unknown as Duck).awwYiss("Motha. Funkin. Bread crumbs!");
    
  3. 修复第三方库的类型。假设类型非常复杂,您没有能力深入研究它们。

您可以定义一个不需要太多运行时成本的虚缩谓词函数:

function narrow<U>(x: any) : x is U { return true }
...
bar(): void {
if (narrow<Duck>(this.animal))
this.animal.quack();
}

但我不认为这是足够接近你的最佳期望。