Typescript 类型谓词结果为 never

Typescript type predicate results in never

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

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,但我没有针对基 class 进行测试!就 instanceof 而言,子 class 是否优先于基数?

我错过了什么?

初步估计,TypeScript 的类型系统是 structural, not nominal。这意味着类型 A 和类型 B 在 TypeScript 中被认为是相同的类型,当且仅当它们具有相同的 结构 ,而不是它们具有相同的 name(或者更准确地说,声明)。这也意味着类型 A 和类型 B 被认为是 不同的 类型当且仅当它们具有 不同的 结构,不仅仅是他们有不同的名字(或声明)。

在您示例的代码中,ClassAClassB 具有相同的结构,因此编译器将它们视为同一类型。如果 x is ClassA returns 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 name 属性 string literal 类型 "A"ClassB name字符串文字类型 "B"。编译器现在将它们视为不同的,一切都很好,对吧?


错了!问题仍然存在,因为 Wrapper<T> 存在同样的问题。在这种情况下,您的 Wrapper<T> class does not depend structurally on T。即使 ClassAClassB 不同,编译器也看不到 Wrapper<ClassA>Wrapper<ClassB> 之间的区别。您可以看出这一点,因为 T 出现在 class 名称中,而在定义中无处可寻。由于类型系统不是名义上的,因此 name Wrapper<ClassA>Wrapper<ClassB> 不同并不重要。它们是同一类型。

我假设您可能希望构造函数参数是 public source: T 而不是 public source: TClass,像这样:

constructor(public source: T) { ... }

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

现在 this在你分别防范​​Wrapper<ClassA>Wrapper<ClassB>后不会降为never

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

好的,希望对你有所帮助。祝你好运!

Link to code