Typescript类型谓词导致永不

以下打字稿片段在严格模式下重现了(编译器)问题,编译后的代码运行良好:

class ClassX
{
    constructor(public label: string) {}
}

class ClassA extends ClassX
{
    constructor() { super('A'); }
}

class ClassB extends ClassX
{
    constructor() { super('B'); }
}

type TClass = ClassA | ClassB;

class Wrapper<T extends TClass>
{
    constructor(public source: TClass)
    {
        if(Wrapper.IsB(this)) console.log(this.source.label);

        // Works normally:
        // if(source instanceof ClassA) this.Log();
        // else if(source instanceof ClassB) this.Log();

        if(Wrapper.IsA(this)) console.log(this.source.label);
        // this results in 'never',would emit error TS2339 without the type guard
        else if(Wrapper.IsB(this)) console.log((this as Wrapper<ClassB>).source.label);
    }

    public static IsA(wrapper: Wrapper<TClass>): wrapper is Wrapper<ClassA>
    {
        return wrapper.source instanceof ClassA;
    }

    public static IsB(wrapper: Wrapper<TClass>): wrapper is Wrapper<ClassB>
    {
        return wrapper.source instanceof ClassB;
    }
}

console.log('ClassA');
new Wrapper(new ClassA()); // logs 'A'

console.log('\nClassB');
new Wrapper(new ClassB()); // logs 'BB'

我怀疑编译器将范围缩小到常见的基本类型ClassX,但是我没有针对基类进行测试!就instanceof而言,子类是否不优先于基类?

我想念什么?

webkate4747 回答:Typescript类型谓词导致永不

首先,TypeScript的类型系统是structural,而不是nominal。这意味着类型A和类型B在TypeScript中被认为是相同的类型,只要且只有它们具有相同的结构,而不是如果它们具有相同的名称(或更准确地说,是声明)。这也意味着类型A和类型B被认为是不同类型,当且仅当它们具有不同结构,而不仅仅是它们具有不同的名称(或声明)。

在您的示例代码中,ClassAClassB具有相同的结构,因此编译器将它们视为相同的类型。如果x is ClassA上的类型防护返回false,则编译器认为x不是ClassA,因此也不是ClassB。这显然不是您的意图;您希望将ClassAClassB视为不同的类型。解决此问题的一种简单方法是设置为add a private property to each class或任意两个不同的属性,例如:

class ClassA extends ClassX {
    readonly name = "A"; // type name is string literal "A"
    constructor() { super('A'); }
}

class ClassB extends ClassX {
    readonly name = "B"; // type name is string literal "B"
    constructor() { super('B'); }
}

这将为ClassA提供string literal类型name的{​​{1}}属性,并且为"A"提供字符串文字类型{{ 1}}。编译器现在将它们视为独特的,一切都很好,对吧?


错了!由于与ClassB相同的问题,问题仍然存在。在这种情况下,您的namedoes not depend structurally on T。即使"B"Wrapper<T>不同,编译器也看不到Wrapper<T>Wrapper<ClassA>之间的区别。您可以这样说是因为Wrapper<ClassB>出现在类名的内部,而在定义的任何地方。而且由于类型系统不是名词性的,所以名称 ClassAClassB不同也没关系。它们是同一类型。

我假设您可能希望构造函数参数为T而不是Wrapper<ClassA>,如下所示:

Wrapper<ClassB>

这将导致public source: T具有类型public source: TClass的{​​{1}}属性,因此constructor(public source: T) { ... } Wrapper<T>将是不同的,因为{{ 1}}将是不同的类型。

现在现在 source不会减少为T,而您分别防范Wrapper<ClassA>Wrapper<ClassB>

source.name

好的,希望能为您提供一些想法。祝你好运!

Link to code

本文链接:https://www.f2er.com/2920984.html

大家都在问