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
被认为是 不同的 类型当且仅当它们具有 不同的 结构,不仅仅是他们有不同的名字(或声明)。
在您示例的代码中,ClassA
和 ClassB
具有相同的结构,因此编译器将它们视为同一类型。如果 x is ClassA
returns false
上的类型保护,则编译器认为 x
不是 ClassA
,因此它也不是 ClassB
.这显然不是您的意图;您希望 ClassA
和 ClassB
被视为不同的类型。解决此问题的一种简单方法是 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
。即使 ClassA
和 ClassB
不同,编译器也看不到 Wrapper<ClassA>
和 Wrapper<ClassB>
之间的区别。您可以看出这一点,因为 T
出现在 class 名称中,而在定义中无处可寻。由于类型系统不是名义上的,因此 name Wrapper<ClassA>
与 Wrapper<ClassB>
不同并不重要。它们是同一类型。
我假设您可能希望构造函数参数是 public source: T
而不是 public source: TClass
,像这样:
constructor(public source: T) { ... }
这将产生 Wrapper<T>
具有类型 T
的 source
属性 的效果,因此 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
}
好的,希望对你有所帮助。祝你好运!
以下打字稿片段在严格模式下重现了(编译器)问题,编译后的代码运行良好:
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
被认为是 不同的 类型当且仅当它们具有 不同的 结构,不仅仅是他们有不同的名字(或声明)。
在您示例的代码中,ClassA
和 ClassB
具有相同的结构,因此编译器将它们视为同一类型。如果 x is ClassA
returns false
上的类型保护,则编译器认为 x
不是 ClassA
,因此它也不是 ClassB
.这显然不是您的意图;您希望 ClassA
和 ClassB
被视为不同的类型。解决此问题的一种简单方法是 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
。即使 ClassA
和 ClassB
不同,编译器也看不到 Wrapper<ClassA>
和 Wrapper<ClassB>
之间的区别。您可以看出这一点,因为 T
出现在 class 名称中,而在定义中无处可寻。由于类型系统不是名义上的,因此 name Wrapper<ClassA>
与 Wrapper<ClassB>
不同并不重要。它们是同一类型。
我假设您可能希望构造函数参数是 public source: T
而不是 public source: TClass
,像这样:
constructor(public source: T) { ... }
这将产生 Wrapper<T>
具有类型 T
的 source
属性 的效果,因此 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
}
好的,希望对你有所帮助。祝你好运!