我试图在打字稿中将类类型设置为通用变量。使用通用版本时,它会显示编译错误。
export class A<T extends Al> { }
export class B extends A<Bl> { }
export class Al { }
export class Bl extends Al { }
show<T extends A<Al>>() {
let t: Type<T>; // "t = B" shows compile error but seem to work
// let t: Type<A<Al>>; // "t = B" shows no compile error and still works
t = B;
}
我还创建了一个stackblitz示例here,以便您了解其工作原理。
我希望如果B extends A<Al>
和T extends A<Al>
,通用版本let t: Type<T>; t = B;
可以正常工作。相反,我必须使用let t: Type<A<Al>>; t = B;
,我想知道是否可以以某种方式使用通用方式?
谢谢。
答案 0 :(得分:1)
此处的示例代码违反了几种TypeScript最佳实践,因此我很难看到您的实际用例是什么
您创建的所有类型都等同于空类型{}
,因此是structurally equivalent to each other。在实践中,这是非常罕见的。即使这里的代码只是作为一个玩具示例,我也怀疑您是指A<T>
,B
,Al
和Bl
完全相同,这是即使您解决了问题中提到的问题,也很可能会导致意外的行为。
通用类A<T>
does not depend structurally on its type parameter。因此,类型A<Al>
与类型A<Bl>
完全相同,类型A<string>
与A<string>
完全相同(您是否认为string
是不可能的,因为Al
不能扩展Al
吗?是的,因为export class A<T extends Al> {
constructor(public t: T) {}
a: string = "a";
}
export class B extends A<Bl> {
constructor(t: Bl) {
super(t);
}
b: string = "b";
}
export class Al {
al: string = "al";
}
export class Bl extends Al {
bl: string = "bl";
}
是空的...请参见前面的要点)。再次,我怀疑您的意思是真的。
因此,我将您的示例代码更改为以下内容:
show()
现在,不同名称的类型实际上在结构上彼此不同,并且泛型类型取决于它们的类型参数。这并不能解决您所要问的问题,但可以阻止编译器在使用类型时出现怪异的行为。
我会注意但不会改变的另外一个:
T
函数在T
中是通用的,但似乎不接受或返回任何取决于类型T
的参数。这很好,因为它只是示例代码,但是编译器无法通过使用show()
来推断T
(请参见the documentation中的“类型参数推断”)。调用时,我们只需手动指定show<T>()
。但是在现实世界的代码中,您希望T
的呼叫签名依赖于T
(或从签名中完全删除function show<T extends A<Al>>() {
let tGood: Type<A<Al>> = B; // okay
let tBad: Type<T> = B; // error!
// 'B' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different
// subtype of constraint 'A<Al>'.
}
)。现在让我们看看发生了什么并检查您得到的实际编译器错误:
tGood
因此,正如您所指出的,Type<A<Al>>
可以工作。类型A<Al>
的意思是“可分配给B
的事物的构造方法。值Type<A<Al>>
是完全有效的B
,因为它是构造方法,并且使{明确定义为扩展A<Al>
的{1}}个实例。
请注意,“ X
扩展了Y
”,“ X
可分配给Y
”和“ X
窄于(或等于)Y
”都(近似)说同一件事:如果您拥有类型X
的值,则可以将其分配给类型Y
的变量。并且扩展/可分配性/狭窄度不是对称的。如果X
扩展了Y
,则Y
扩展了X
不一定是正确的。作为示例,请考虑将string
分配给string | number
(例如,const sn: string | number = "x"
可以),但是不能将string | number
分配给string
(例如,{ {1}}是一个错误)。
所以现在看const s: string = Math.random()<0.5 ? "x" : 1
。这是一个错误,因为tBad
是某种通用类型,而我们所知道的只是T
扩展 T
。它不一定与A<Al>
相等。实际上,它可能是A<Al>
的严格狭窄子类型(即A<Al>
扩展了{{1} }并不意味着T
扩展了A<Al>
)。因此我们无法将A<Al>
分配给T
,因为B
的实例可能最终不会成为Type<A<Al>>
的实例。 (错误消息明确指出:B
)
让我们在这里举一个具体的例子:
T
您可以看到'T' could be instantiated with a different subtype of constraint 'A<Al>'
的实例具有一个名为export class C extends A<Al> {
constructor(t: Al) {
super(t);
}
c: string = "c";
}
show<C>(); // no error, but B does not create instances of C
的字符串属性。但是C
构造函数不会使用c
属性创建实例。我可以呼叫B
,将c
指定为show<C>()
。之所以可以接受,是因为T
扩展了C
,但是在实现过程中,C
本质上是在说A<Al>
,这是不正确的。因此是错误。
现在,由于我无法告诉您示例代码的用途,因此我不确定如何建议对其进行修复。当然,您可以使用let t: Type<T> = B
。完全删除泛型即可解决此问题。如果您需要继续使用泛型,则可能需要传递这些泛型类型的参数。就是这样:
let t: Type<C> = B
如果您需要一个tGood
,那么您将不得不传递一个……您不能仅仅希望function showMaybe<T extends A<Al>>(ctor: Type<T>) {
let tFine: Type<T> = ctor;
}
是一个。同样,我不知道上面的方法是否对您有用,但是类似的方法可能是解决方法。
如果您只是想让编译器警告消失,并且确信您所做的实际上是安全的,则可以使用type assertion:
Type<T>
这将不再产生错误,但是具有与以前相同的所有问题。您仍然可以调用B
,并且根据对function showUnsafe<T extends A<Al>>() {
let tUnsafe: Type<T> = B as any as Type<T>; // unsafe assertion
}
的处理方式,您可能最终会向编译器撒谎,即构造实例的类型实际上为{{ 1}}。使用类型断言时,您将类型安全的责任从编译器转移到您身上,因此,只有当断言为假并造成不愉快的运行时后果时,您才应责怪自己。
好的,希望这对您有所帮助。祝你好运!