在if-else语句

时间:2017-07-28 19:33:09

标签: typescript instanceof type-narrowing

当我尝试在if-else语句中使用instanceof派生类实例时遇到问题。请考虑以下示例:

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部分的obj无法调用else,因为它缩小为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进行了测试。

谢谢!

1 个答案:

答案 0 :(得分:1)

欢迎来到TypeScript Issue #7271的世界!你被TypeScript' structural typing及其与instanceof的奇怪(和坦率的不健全)互动所咬。

TypeScript将Derived1Derived2视为完全相同的类型,因为它们具有相同的结构形状。如果obj instanceof Derived1返回false,那么TypeScript编译器会同时认为"好的,obj不是Derived1"和#34;好的,obj不是Derived2",因为它没有看到它们之间的区别。然后当您检查obj instanceof Derived2返回true时,编译器说" Gee,obj两者都是Derived2。这可能会never发生。"当然, 在运行时Derived1之间存在差异,并且可以发生。这是你的问题。

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

Derived2

现在,每个类上都有一个可选的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... } } 属性,具有不同的字符串文字类型(不更改发出的JavaScript)。 TypeScript现在意识到typeDerived1不同,您的错误就会消失。

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

更新1

@ sebastien-grenier said

  

感谢您的解释!但是,当覆盖中的参数类型不同时,我没有看到为什么Typescript认为它们在结构上相同,但是当类型相同时(即与父代Derived2相同),一切都编译得很好。另外,如果我的对象上已经有一个名为IBaseModel的成员,会发生什么?它可能与type发生冲突吗? ?。谢谢!

哇,这很奇怪。看起来在某些时候有一个change (#10216)来修复问题#7271的一些实例,但是您设法找到了一个新实例。我的猜测就是因为你用较窄的参数类型覆盖了type方法(顺便说一下......顺便说一下......每个setModel应该有一个BaseClass接受任何 setModel()。如果你对我们这样做很有兴趣,我们可以谈谈),它会将#10216中的代码更改变为不适用。这可能是一个错误...你可能想要file it

是的,如果您已拥有具有相同密钥的属性,则应选择一个新密钥。我们的想法是品牌化。如果您担心意外冲突,可以选择IBaseModel这样的名称。

但是你可以做的更直接的改变不会发生冲突:

__typeBrand

据推测,你希望每个班级都知道它的class Derived1 extends BaseClass { model: IDerived1Model; // your overrides follow } class Derived2 extends BaseClass { model: IDerived2Model; // your overides follow } 是缩小的类型,对吗?因此,执行上述model的缩小都可以让编译器知道类型在结构上是不同的,并使派生类更安全。

干杯!