TypeScript 类参数错误地被当作双变体处理

j2datikz  于 6个月前  发布在  TypeScript
关注(0)|答案(9)|浏览(54)

🔎 搜索词

class parameter generic bivariant constructor typeof extends

🕗 版本与回归信息

  • 这是我在每个版本中尝试的行为

⏯ Playground链接

https://www.typescriptlang.org/play?ts=5.3.0-dev.20231026#code/C4TwDgpgBAggRgZ2AJwIYGNirgGwgYQHsA7JZAV00OQB4BLUrY9aAXigG8BfAPinexkMwKMQgB3KAAoAsACgAkADoVqZAHMEALlEQAbhGQBtALryAlPz4MkqZhHnzQkKAElGdlgHkAZjXQkZJTA1Hys8goBjBRUyFAQAB7AEMQAJgiwiCjC2HhE0cHU9MQ+hlA2TCx8APzlHvZQOmIGyI5y6DioCBkAYoSEnBGpEB1q0HBqOijkDnJcbaPdUAAKyIQhNGBrIfFJKemZQpi5BIHTsfyH2ce4pwWxfBxDI53I0BWeEDrutva+m9tCDx5PM5PIAPTgqBeADWTnA0Bgl1W60IRgARB97OizHJIVACQA9aptfGw+EuABCyMBNGcEEIPigfSBGKxLBxEKhRJJYLxUPJcnpUHwlyRiWSaQy1Nq02gOh8qBwCFm+J5pKhABUABZ0DIIbWEcg4VJQODQRXK2bCgBaNNRu0lBxRG3pjOZ-RqUDljSglpVXIJUGJQA

💻 代码

type AbstractableConstructor<instance = {}> = abstract new (
	...args: never[]
) => instance

type InstanceOf<constructor> =
	constructor extends AbstractableConstructor<infer instance> ? instance : never

class Foo {
	declare bar: true
}

class Proto<proto extends AbstractableConstructor = AbstractableConstructor> {
	declare instance: InstanceOf<proto>
}

// Ok
type A = Proto["instance"]
//   ^? {}

// Ok
type B = Proto<typeof Foo>["instance"]
//   ^? Foo

// Ok
type C = A extends B ? true : false
//   ^? false

// This should be false
type Z = Proto extends Proto<typeof Foo> ? true : false
//   ^? true

🙁 实际行为

proto 被当作bivariant处理,没有结构比较,导致 Z 评估为 true

🙂 预期行为

proto 应该被视为协变,因为只使用其返回类型或结构上进行比较,导致 Z 评估为 false

关于问题的附加信息

  • 无响应*
qybjjes1

qybjjes11#

相当奇怪。幸好,通过在 proto 上添加一个 out 注解,可以轻松解决这个问题。

whitzsjs

whitzsjs2#

这里的问题是,方差计算将 proto 类型参数归类为独立(即未被观察到,除了那些扩展了 AbstractableConstructor 的类型之外)。不清楚我们是否能在这里做得更好,毕竟 proto 唯一的用途是在 InstanceOf 中作为推理的来源,我们无法测量这种条件的方差。

bt1cpqcv

bt1cpqcv3#

正如@RyanCavanaugh所说,一旦你知道这是发生的事情,解决方法就很简单了,但要达到这一点?在现实世界的情况下可能就不那么简单了。
@Andarist提到这可能与可变性“无法衡量”有关。如果不能准确推断,我宁愿在允许这样的赋值时明确地进行类型转换,而不是在不需要时得到一个错误的阳性结果。
在这种情况下,有没有一种方法可以不破坏大量现有代码的情况下将参数视为不变而不是双变?

bweufnob

bweufnob4#

我们可能会考虑在测量变异时使用某种诊断方法,但这通常不是有意为之的。然而,毫无疑问,这将是一个破坏性的改变,并且需要在(另一个)编译器开关下进行。
或者,这可以是一个lint问题,只要我们的API中暴露了必要的类型检查器信息。

xmjla07d

xmjla07d5#

实际上,我们测量 InstanceOf 为双变性,因此 Proto 也会测量双变性。双变性来自于规则的影响:

// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.

似乎有点任意地选择双变性,因为我们没有使用用于方差探测的标记类型来衡量结果之间的差异。实际上,我们是在说,当我们无法测量条件类型的方差时,我们认为该条件类型是双变性的。

czfnxgou

czfnxgou6#

在这里,InstanceOf被正确地视为协变。

type T1 = InstanceOf<AbstractableConstructor> extends InstanceOf<typeof Foo> ? true : false;
//   ^? type T1 = false
type T2 = InstanceOf<typeof Foo> extends InstanceOf<AbstractableConstructor> ? true : false;
//   ^? type T2 = true
but5z9lq

but5z9lq7#

InstanceOf 在这里被正确地视为协变。
是的,这里发生的是 InstanceOf<AbstractableConstructor>InstanceOf<typeof Foo> 在条件类型被评估之前被解析为 {}Foo。所以,这就像写 {} extends Foo ? true : false 一样,在那个时候我们甚至不参考计算出的 InstanceOf 的变异性。
但是在涉及两个 Proto 示例的情况下,我们确实会参考变异性信息,并且,由于它声称双变性,我们认为 Proto<AbstractableConstructor>Proto<typeof Foo> 在两个方向上都相关。你可以通过强制结构比较来看到区别:

class Proto2<proto extends AbstractableConstructor = AbstractableConstructor> {
	declare instance: InstanceOf<proto>
}

type Z2 = Proto extends Proto2<typeof Foo> ? true : false
//   ^? false

理想情况下,我们应该检测到条件类型变异性无法测量的情况,并强制进行结构比较,但我们在过去尝试过,结果对性能影响非常糟糕。

92dk7w1h

92dk7w1h8#

我发现这个有点难以理解,所以整理了一个更简单的复现。我想在这里发布它,以防它对其他人有帮助:

type Id<T = {}> = T

type ConditionalId<T> = T extends Id<infer Value> ? Value : never

class Box<B> {
  declare value: ConditionalId<B>
}

// Ok
type A = Box<{}>["value"]
//   ^? {}

// Ok
type B = Box<{ foo: true }>["value"]
//   ^? { foo: true }

// Ok
type C = A extends B ? true : false
//   ^? false

// This should be false
type Z = Box<{}> extends Box<{ foo: true }> ? true : false
//   ^? true
zzwlnbp8

zzwlnbp89#

另一种解决方法,不需要手动差异注解,是将内联类型表达式提取到一个新的类型变量中。

// change this:
class Proto<proto extends AbstractableConstructor = AbstractableConstructor> {
  declare instance: InstanceOf<proto>
}

// to this:
class Proto<proto extends AbstractableConstructor = AbstractableConstructor, T = InstanceOf<proto>> {
  declare instance: T
}

或者,在简化的例子中:

// change this
class Box<B> {
  declare value: ConditionalId<B>
}

// to this
class Box<B, T = ConditionalId<B>> {
  declare value: T
}

也就是说,类型相对于 proto / B有条件的独立的,给定 T 。这似乎比结构比较便宜得多,而且非常简单,可以自动完成。

相关问题