A 什么时候不在 TypeScript 中扩展 A

When does A not extends A in TypeScript

出于与问题无关的原因(但其中包括类型级编程中的乐趣和收益),我的一种类型最终归结为以下最小示例:

type IsTrue<A extends true> = A
type Refl<M> = M extends M ? true : false
type Proof<M> = IsTrue<Refl<M>>

... 导致编译错误。现在,我们可以讨论我是如何走到这里的,与一种(可能)错误的类型相等编码方式有关。但问题仍然存在:什么时候 M extends M 不会对所有 M 解析为 true?反例是什么?如何解决这个问题(可能通过约束 M)?

Conditional types, as originally implemented in microsoft/TypeScript#21316 并不总是被 热切地 评估。如果它们依赖于 as-yet-unspecified 泛型类型参数,例如 Refl<M> 定义主体中的 M,编译器通常会 延迟 评估条件类型。请注意,关于编译器将在何时何地评估条件类型的确切细节在任何文档中都没有详细说明,并且它们随着时间的推移随着 TypeScript 的新版本而不断发展,所以我不能在这里绝对肯定地说任何事情。不过大体情况就是我描述的那样。

您期望编译器查看 M extends M ? true : false 并急切地将其缩减为 true 以便您的定义等同于 type Refl<M> = true,或者在 [=13] 的定义中=] 或在 Proof<M> 的定义中。这些评估都没有发生;它们被推迟是因为在这两种情况下 M 都是未指定的类型参数。因此编译器无法确定 Refl<M> 是否比 IsTrue<A extends true>.

union true | false (also known as the boolean type), and thus is not known to satisfy the constraint 更窄

所以并不是说 M extends M ? true : false 应该实际评估为 false(据我所知它不能),而是编译器根本无法按顺序评估它将其简化为 true.

我没有看到关于这种特定情况的任何 GitHub 问题,但有许多此类问题归结为编译器无法分析依赖于未解析泛型类型参数的条件类型。有关较新的示例,请参阅 microsoft/TypeScript#46795.


请注意,M extends ... ? ... : ... 的特定形式 M 是普通泛型类型参数,称为 distributive conditional type,因此 M 中的任何联合都将被分解为在被评估之前的个人成员。这不会影响 Refl<M> 是否可以比 true 更宽,但它会影响输出类型:

type Refl<M> = M extends M ? true : false
type Hmm = Refl<never> // never
type Refl2<M> = [M] extends [M] ? true : false;
type Hmm2 = Refl2<never> // true

Refl<M> 分配给 M 中的联合,never 被认为是“空联合”(参见 this comment in ms/TS#23182),因此输出也是空联盟。但是 Refl2<M> 不是分配的(因为 [M] 不是普通的泛型类型参数)所以 Refl2<never>true。但是,nevertrue 都可以分配给 true,因此 IsTrue<Refl<M>> 无论如何都可以解决。但这比证明这一点要复杂得多。


可以想象,可以引入一个特性,在 Y 不依赖于 [=46] 的情况下,形式 X extends X ? Y : Z 的条件类型可以急切地减少到 Y =] (你的情况)或 X 不是普通泛型类型参数的地方(不是你的情况)。但是这样的功能会对编译器性能产生负面影响,因为它需要针对这种情况检查每个条件类型,即使绝大多数条件类型都不是这样;功能需要为自己买单,而这个可能不会。更糟糕的是,可能有很多 real-world 代码有意或无意地依赖于编译器延迟这样的条件类型,这样的功能将是一个巨大的突破性变化。


最后,如果您只是对解决方法感兴趣,我通常的做法如下:如果您确定 T extends U 但编译器不是,那么您不能使用 T 在一个期望可以分配给 U 的地方。但是你可以使用Extract<T, U>Extract<T, U> utility type is primarily meant to filter any unions in T so that only those members assignable to U are left. But the compiler does see Extract<T, U> is assignable to both T and U (possibly in ms/TS#29437),如果 T extends U 是正确的,那么 Extract<T, U> 最终将根据需要评估为 T。所以这个 off-label 使用 Extract 做我们想要的:

type Proof<M> = IsTrue<Extract<Refl<M>, true>> // okay

这几乎类似于 type-level type assertion,因此适用类似的注意事项。如果你认为 Refl<M> 可分配给 true 是错误的,那么 IsTrue<Extract<Refl<M>, true>> 仍然可以编译,但你不再评估 IsTrue<Refl<M>>,而是更像 [=66] =] 或 IsTrue<true>。所以要小心!

Playground link to code