考虑这种常见类型
UnionToIntersection
:
type UnionToIntersection<T> =
(T extends any ? (a: T) => void : never) extends (a: infer I) => void ? I : never;
根据我的理解,第一个“步骤”强制打字稿将给定的联合
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
。
当我们将函数联合的创建分解为单独的步骤时,这不起作用:
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;
为什么我忽略了导致这种行为差异的机制或功能?
两个版本的
UnionToIntersection
之间的区别在于,具有中间命名类型别名的版本已将内部条件类型变成了分布式条件类型。也就是类型
type UnionOfFunctionsToIntersection<T> = T extends (arg: infer I) => void ? I : never;
将 distribute over T
中的
unions,因为
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;
类型 Z = UnionOfFunctionsToIntersection// 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 代码链接