在 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 将 Derived1
和 Derived2
视为完全相同的类型,因为它们具有相同的结构形状。如果 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." 当然 是 Derived1
和 Derived2
在运行时,并且 可以 发生。哪个是你的问题。
解决方案:将一些不同的 属性 推到 Derived1
和 Derived2
中,以便 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 现在意识到 Derived1
与 Derived2
不同并且您的错误消失了。
希望对您有所帮助。祝你好运!
更新 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 使用起来更安全。
干杯!
当我尝试在 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 将 Derived1
和 Derived2
视为完全相同的类型,因为它们具有相同的结构形状。如果 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." 当然 是 Derived1
和 Derived2
在运行时,并且 可以 发生。哪个是你的问题。
解决方案:将一些不同的 属性 推到 Derived1
和 Derived2
中,以便 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 现在意识到 Derived1
与 Derived2
不同并且您的错误消失了。
希望对您有所帮助。祝你好运!
更新 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 calledtype
on my object? Can it conflict withtype
? ?. 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 使用起来更安全。
干杯!