打字稿类型谓词导致从不

cha*_*iik 3 predicate instanceof 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'

Run Code Online (Sandbox Code Playgroud)

我怀疑编译器正在缩小到 common base type ClassX,但是我没有针对基类进行测试!instanceof就相关而言,子类是否优先于基类?

我错过了什么?

jca*_*alz 6

大致来说,TypeScript 的类型系统是结构化的,而不是名义上的。这意味着 typeA和 typeB在 TypeScript 中被认为是相同的类型,当且仅当它们具有相同的结构,而不是如果它们具有相同的名称(或更准确地说,声明)。这也意味着 typeA和 typeB被认为是不同的类型,当且仅当它们具有不同的结构,而不仅仅是它们具有不同的名称(或声明)。

在您示例中的代码中,ClassA并且ClassB具有相同的结构,因此编译器将它们视为相同的类型。如果x is ClassA返回的类型保护false,则编译器认为x不是ClassA,因此也不是ClassB。这显然不是你的本意;你想要ClassA并被ClassB认为是不同的类型。解决此问题的一种简单方法是为每个 class或任何两个不同的属性添加一个private属性,如下所示:

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'); }
}
Run Code Online (Sandbox Code Playgroud)

这给了ClassAname对财产字符串字面"A",和ClassB一个name字符串文字类型"B"。编译器现在认为它们是不同的,一切都很好,对吧?


错误的!问题仍然存在,因为与Wrapper<T>. 在这种情况下,您的Wrapper<T>在结构上不依赖于T. 编译器看不出有什么区别Wrapper<ClassA>Wrapper<ClassB>甚至当ClassAClassB不同时。您可以说出这一点,因为它T显示在类名中,而在定义中没有显示。而且由于类型系统不是名义上的,因此名称 Wrapper<ClassA>Wrapper<ClassB>. 他们是同一类型。

我假设您可能希望构造函数参数是public source: T而不是public source: TClass,如下所示:

constructor(public source: T) { ... }
Run Code Online (Sandbox Code Playgroud)

这将具有Wrapper<T>具有sourcetype 属性的效果T,因此Wrapper<ClassA>Wrapper<ClassB>将是不同的,因为source.name将是不同的类型。

并且现在 在您防范和分开之后this不会减少到:neverWrapper<ClassA>Wrapper<ClassB>

constructor(public source: T) {
    if (Wrapper.IsA(this)) console.log(this.source.label);
    else if (Wrapper.IsB(this)) console.log((this).source.label); // okay
}
Run Code Online (Sandbox Code Playgroud)

好的,希望能帮到你一些想法。祝你好运!

代码链接