网站搜索

typescript - 为什么在类型分解过程中联合类型会“塌缩”?


考虑这个常见的类型 UnionToIntersection:

type UnionToIntersection<T> =
    (T extends any ? (a: T) => void : never) extends (a: infer I) => void ? I : never;

据我理解,第一步会强制 TypeScript 将给定的联合类型 T 分发为相应的 (a: T) => void 类型的联合。例如,(a: A) => void | (b: T) => void 等。然后,由于 TypeScript 自然处理函数联合的参数方式,推断出的 I 是 T 的交叉类型。

例如,如果我们有这个:

type F1 = (arg: A) => void;
type F2 = (arg: B) => void;
const eitherF1orF2: F1 | F2 = (arg: any) => {}

我们的eitherF1orF2函数要求arg参数为A & B。

当我们将函数 union 的创建拆分为一个单独的步骤时,这不起作用:

type UnionOfFunctions<T> = T extends any ? (arg: T) => void : never
type UnionOfFunctionsToIntersection<T> = T extends (arg: infer I) => void ? I : never;
type UnionToIntersection<T> = UnionOfFunctionsToIntersection<UnionOfFunctions<T>>;

这个分解版本只是返回原始的联合类型。

值得注意的是,如果我将这些稍有不同的组合起来,它又能按预期工作:

type UnionToIntersection<T> = UnionOfFunctions<T> extends (arg: infer I) => void ? I : never;

我忽略了哪种机制或功能导致了这种行为差异?

答案#1

你的两个 UnionToIntersection 版本之间的区别在于,带有中间命名类型别名的那个版本将内部条件类型变成了分配式条件类型。也就是说,该类型

type UnionOfFunctionsToIntersection<T> = T extends (arg: infer I) => void ? I : never;

将会在 T 中的联合类型上 分发,因为 T 是一个裸泛型类型参数。所以 UnionOfFunctionsToIntersection<X | Y | Z> 必然会被求值为 UnionOfFunctionsToIntersection<X> | UnionOfFunctionsToIntersection<Y> | UnionOfFunctionsToIntersection<Z>,无论如何:

type Z = UnionOfFunctionsToIntersection<F1 | F2> // A | B

他 infer 表达式从一开始就从不作用于联合类型。因此它不可能产生从逆变位置推断出的类型交叉。

另一方面,在原始 UnionToIntersection 的类似部分

type UnionToIntersection<T> =
    (T extends any ? (a: T) => void : never) extends (a: infer I) => void ? I : never;

正在检查的类型是 (T extends any ? (a: T) => void : never),它不是一个单独的类型参数。因此,它不是分布式条件类型,并且不会在该类型中的联合类型上进行分布。因此,如果它生成一个联合类型,infer I 将在联合类型上操作,并且它有机会从逆变位置的类型中推断出一个交集。


要修复此问题,使两个版本等效,你应该更改 UnionOfFunctionsToIntersection<T>,使其不再对 T 分发。最简单的方法是将检查的双方都包裹在一个单元素元组中,例如

type UnionOfFunctionsToIntersection<T> = [T] extends [(arg: infer I) => void] ? I : never;

type Z=UnionOfFunctionsToIntersection<F1 | F2>/A & B

一般来说,如果你有一个类型像

type Foo<T> = Bar<T> extends ⋯ ? ⋯ : ⋯

并想要重构它为

type Baz<T> = T extends ⋯ ? ⋯ : ⋯
type Foo<T> = Baz<Bar<T>>

如果你关心联合体的行为,你应该确保你的中间类型是非分配的:

type Baz<T> = [T] extends [⋯] ? ⋯ : ⋯
type Foo<T> = Baz<Bar<T>>

这一切与 TypeScript 在你尝试 调用 函数组合时的行为几乎没有关系,如你在 eitherF1orF2 示例中所示:

type F1 = (arg: A) => void;
type F2 = (arg: B) => void;
declare const eitherF1orF2: F1 | F2;

declare const a: A;
eitherF1orF2(a); // error
// Argument of type 'A' is not assignable to parameter of type 'A & B'.

declare const b: B;
eitherF1orF2(b); // error
declare const ab: A & B;

// Argument of type 'B' is not assignable to parameter of type 'A & B'.
eitherF1orF2(ab); // okay

这里要求参数的类型必须是各个参数类型的交集,如果你使用 IntelliSense 检查该参数,你会看到这个交集。但这并不能直接对应于 infer 在条件类型中的工作方式。确实,这两种行为都涉及协变性将联合类型转为交集类型,但它们发生在不同的上下文中。也就是说,这两个特性的底层类型理论是相同的,但它们是无关的实现。

实际上,在 TypeScript 3.3 引入对调用函数联合的支持之前,对 eitherF1orF2 的所有三次调用都会失败,因为 F1 和 F2 的签名不同。根本不存在交集,只是失败而已。但在 TypeScript 2.8 中引入的条件类型行为仍会推断出一个交集。

所以 eitherF1orF2 的行为在这里有点像红鲱鱼;你实际上不能用它来得出关于 UnionToIntersection 是如何工作的(或无法工作的)太多结论。

代码的 Playground 链接