在 if-else 语句中使用 instanceof 将 Typescript 类型缩小为 never

Typescript type narrowed to never with instanceof in an if-else statement

当我尝试在 if-else 语句中将 instanceof 与派生的 class 实例一起使用时遇到问题。考虑以下示例:

interface IBaseModel {
    id: string
}

class BaseClass {
    model: IBaseModel
    constructor() {
    }

    setModel(model: IBaseModel) {
        this.model = model
    }

    getValueByName(name: string) {
        return this.model[name];
    }
}

interface IDerived1Model extends IBaseModel {
    height: number;
}

class Derived1 extends BaseClass {
    setModel(model: IDerived1Model) {
        super.setModel(model);
        // Do something with model...
    }
}

interface IDerived2Model extends IBaseModel {
    width: number;
}

class Derived2 extends BaseClass {
    setModel(model: IDerived2Model) {
        super.setModel(model);
        // Do something with model...
    }
}

const model1 = { id: "0", height: 42 };
const model2 = { id: "1", width: 24 };

const obj1 = new Derived1();
obj1.setModel(model1);

const obj2 = new Derived2();
obj2.setModel(model2);

const objs: BaseClass[] = [
    obj1,
    obj2
];

let variable: any = null;
for (const obj of objs) {
    if (obj instanceof Derived1) {
        variable = obj.getValueByName("height"); // Ok, obj is now of type `Derived1`
    } else if (obj instanceof Derived2) {
        variable = obj.getValueByName("width"); // Does not compile: Property 'getValueByName' does not exist on type 'never'
    }
    console.log("Value is: " + variable);
}

这里,getValueByName 不能在 else 部分的 obj 上调用,因为它被缩小到 never。不知何故,Typescript 认为 else 永远不会被执行,但这是错误的。

要注意的重要事项是覆盖函数 setModel。覆盖具有不同的参数类型,但这些类型继承自基本 IBaseModel 类型。如果我将它们更改为基本类型,Typescript 不会抱怨并且可以正常编译:

class Derived1 extends BaseClass {
    setModel(model: IBaseModel) {
        super.setModel(model);
        // Do something with model...
    }
}

class Derived2 extends BaseClass {
    setModel(model: IBaseModel) {
        super.setModel(model);
        // Do something with model...
    }
}

所以我的问题是,为什么具有不同类型的覆盖会使 instanceof 运算符将对象的类型缩小为 never?这是设计使然吗?

已使用 Typescript 2.3.4、2.4.1 和 Typescript Playground 进行测试。

谢谢!

欢迎来到 TypeScript Issue #7271! You've been bitten by TypeScript's structural typing 的世界及其与 instanceof 的奇怪(而且坦率地说是不合理的)互动。

TypeScript 将 Derived1Derived2 视为完全相同的类型,因为它们具有相同的结构形状。如果 obj instanceof Derived1 returns false,TypeScript 编译器认为 "Okay, obj is not a Derived1" 和 "Okay, obj is not a Derived2",因为它看不出它们之间的区别。然后当你检查 obj instanceof Derived2 返回 true 时,编译器说 "Gee, obj both is and is not a Derived2. That can never happen." 当然 Derived1Derived2 在运行时,并且 可以 发生。哪个是你的问题。

解决方案:将一些不同的 属性 推到 Derived1Derived2 中,以便 TypeScript 可以区分它们。例如:

class Derived1 extends BaseClass {
    type?: 'Derived1'; // add this line
    setModel(model: IDerived1Model) {
        super.setModel(model);
        // Do something with model...
    }
}

class Derived2 extends BaseClass {
    type?: 'Derived2'; // add this line
    setModel(model: IDerived2Model) {
        super.setModel(model);
        // Do something with model...
    }
}

现在每个 class 上都有一个可选的 type 属性,具有不同的字符串文字类型(不更改发出的 JavaScript)。 TypeScript 现在意识到 Derived1Derived2 不同并且您的错误消失了。

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


更新 1

@sebastien-grenier :

Thanks for the explanation! However, I fail to see why Typescript considers them structurally identical when the types of the parameter in the override is different, but everything compiles fine when the type is identical (i.e. the same as the parent, IBaseModel). Also, what happens if I already have a member called type on my object? Can it conflict with type? ?. Thanks!

哇,真奇怪。好像有一个change (#10216) at some point to fix some instances of issue #7271, but you managed to find a new one. My guess is that because you override the setModel method with a narrower argument type (which is unsound, by the way... every BaseClass should have a setModel() that accepts any IBaseModel. If you're interested in doing this soundly we can talk), it fools the code change in #10216 into not applying. This might be a bug... you may want to file it

是的,如果您已经有一把带有相同钥匙的 属性,您应该选择一把新的。这个想法是为类型打上烙印。如果您担心意外冲突,可以选择 __typeBrand 这样的名称。

但是您可以进行更直接且不会冲突的更改:

class Derived1 extends BaseClass {
    model: IDerived1Model;
    // your overrides follow
}

class Derived2 extends BaseClass {
    model: IDerived2Model;
    // your overides follow
}

大概你想要每个class知道它的model是缩小类型,对吧?因此,对 model 进行上述缩小既让编译器知道类型在结构上是不同的,又使派生的 class 使用起来更安全。

干杯!