将联合类型转换为交集类型

Tit*_*mir 55 typescript

有没有办法将联合类型转换为交集类型:

type FunctionUnion = () => void | (p: string) => void
type FunctionIntersection = () => void & (p: string) => void
Run Code Online (Sandbox Code Playgroud)

我想应用转换FunctionUnion来获取FunctionIntersection

jca*_*alz 113

你想要工会交汇吗? 分配条件类型条件类型的推断可以做到这一点.(不要认为有可能做交叉联合,抱歉)这是邪恶的魔法:

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
Run Code Online (Sandbox Code Playgroud)

分配联盟U并将其重新包装成一个新的联盟,所有的成员都处于逆境.这允许将类型推断为交集I,如手册中所述:

同样地,在反变量位置中相同类型变量的多个候选者导致推断交叉类型.


让我们看看它是否有效.

首先让我用括号括起你FunctionUnion,FunctionIntersection因为TypeScript似乎比函数返回更紧密地绑定了union/intersection:

type FunctionUnion = (() => void) | ((p: string) => void);
type FunctionIntersection = (() => void) & ((p: string) => void);
Run Code Online (Sandbox Code Playgroud)

测试:

type SynthesizedFunctionIntersection = UnionToIntersection<FunctionUnion>
// inspects as 
// type SynthesizedFunctionIntersection = (() => void) & ((p: string) => void)
Run Code Online (Sandbox Code Playgroud)

看起来不错!

请注意,通常会UnionToIntersection<>公开TypeScript认为是实际联合的一些细节.例如,boolean显然在内部表示为true | false,所以

type Weird = UnionToIntersection<string | number | boolean>
Run Code Online (Sandbox Code Playgroud)

type Weird = string & number & true & false
Run Code Online (Sandbox Code Playgroud)

希望有所帮助.祝好运!

  • 10倍 我总是向您学习新的有趣的事情。我在这个问题上非常接近/sf/ask/3525850961/#50375712但确实需要一种方法将工会转变为十字路口 (8认同)
  • 这是因为在条件类型中`extends`之前的裸类型参数是[分布式](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#distributive-conditional-types)跨任何联合成分.如果要禁用分布式条件类型,可以使用使类型参数"clothed"的技巧,例如像这样的单元素元组:`type Param <T> = [T] extends [(arg:infer U) )=>无效]?你永远不会;`.这应该按照你想要的方式工作. (5认同)
  • @RanLottem 关键是[分布式条件类型](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#distributive-conditional-types)。在我看来,手册解释得很好。我已经[在其他地方对其进行了扩展](/sf/answers/3876867151/) 您需要更多信息。祝你好运! (3认同)
  • 在这两个示例中,相关类型处于逆变位置(函数类型的参数)。如果您有类似 `(k: A)=&gt;void | 的类型 (k: B) =&gt; 无效 | (k: C) =&gt; void` 并从中推断出 `(k: infer I) =&gt; void`,唯一合理的推断是 `I` 是 `A`、`B` 和 ` 的*交集* C`。当然 `((k: A) =&gt; void) | (k: B) =&gt; void)` *不能*分配给 `(k: A | B) =&gt; void`;请参阅https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#improved-behavior-for-calling-union-types (3认同)
  • 这个答案很棒,但是我真的很难理解这部分“如何分发联合U并将其重新包装成新的联合,其中所有成分都处于**变位**”的工作原理:(我无法完全掌握我认为这段代码:`type Param &lt;T&gt; = Textended(arg:infer U)=&gt; void?U:never;``type InferredParams = Param &lt;(((a:string)=&gt; void)|(((a:number)=&gt; void)&gt; ;;应该给我`string&number`,但是却给我`string | number`。您能解释为什么吗? (2认同)
  • 您是我不断学习新TS技巧的人。:) (2认同)
  • 你能解释一下开头部分“U extends any 吗?”?我尝试了 `((a: U) =&gt; void) extends (a: infer I) =&gt; void ?I : never;` 但它不起作用,即使生成的类型 U =&gt; void 应该是相同的。 (2认同)
  • @NicholasCuthbert 小心;“未知”是“正确”的结果。对于所有“A”和“B”,“UTI&lt;A | B&gt;` 应该等于 `UTI&lt;A&gt; &amp; UTI&lt;B&gt;`,对吧?令“B”为“从不”并记下“A | 从不`是`A`。现在,所有“A”都有“UTI&lt;A&gt; = UTI&lt;A&gt; &amp; UTI&lt;never&gt;”。满足这一点的唯一合理方法是“UTI&lt;never&gt;”=“unknown”,因为“X &amp;unknown”等于“X”。如果“UTI&lt;never&gt;”是“never”,则“UTI&lt;A&gt; = UTI&lt;A&gt; &amp; never”,但由于“X &amp; never”=“never”,所以你会说“UTI&lt;A&gt; = never” ` 对于所有的 `A`...不太好。可能存在“UTI&lt;never&gt;”成为“never”的用例,但对我来说并不明显,所以要小心。 (2认同)
  • @Ferrybig 看起来像是带有条件类型和编译器标志的编译器错误,然后,请参阅[此处](https://tsplay.dev/mbd64w)。如果还没有的话,也许有人应该打开一个关于它的 GitHub 问题。 (2认同)
  • 我正在尝试隔离问题,看起来当启用“--exactOptionalPropertyTypes”时,函数类型在“undefined”周围有一些奇怪的行为。 (2认同)
  • @Ferrybig 好吧,看起来这根本与函数类型无关,但事实是编译器在交叉点上做了奇怪的事情......请参阅[此评论](https://github.com/microsoft/TypeScript/issues/45623 #issuecomment-931389122) 我添加到了 ms/TS#45623,这是一份现有的错误报告。我认为这可能是相同的,已经报告的错误,但如果不是,我会在某个时候打开一个新的错误。 (2认同)
  • @Josep,虽然 jcalz 的回答对您很有帮助,但请注意,如果您在短时间内对他们的 2 个以上帖子进行了投票,那么您实际上会给他们带来不好的体验,因为系统会将其视为有针对性的投票并会在下一个 UTC 日运行撤销脚本后自动恢复这些投票。另请注意,连续投票会让您受到主持人的关注,因为这被视为滥用 - 很高兴您的行为不是恶意的,因此他们不太可能受到任何处罚,但请在将来牢记这一点。 (2认同)

pol*_*.ph 8

当您想要几种类型的交集,但不一定将联合转换为交集时,还有一个非常相关的问题。如果不求助于临时工会,就没有办法直接进入十字路口!

问题是我们想要得到交集的类型可能在 内部有联合,这些联合也会被转换为交集。守卫救援:

// union to intersection converter by @jcalz
// Intersect<{ a: 1 } | { b: 2 }> = { a: 1 } & { b: 2 }
type Intersect<T> = (T extends any ? ((x: T) => 0) : never) extends ((x: infer R) => 0) ? R : never

// get keys of tuple
// TupleKeys<[string, string, string]> = 0 | 1 | 2
type TupleKeys<T extends any[]> = Exclude<keyof T, keyof []>

// apply { foo: ... } to every type in tuple
// Foo<[1, 2]> = { 0: { foo: 1 }, 1: { foo: 2 } }
type Foo<T extends any[]> = {
    [K in TupleKeys<T>]: {foo: T[K]}
}

// get union of field types of an object (another answer by @jcalz again, I guess)
// Values<{ a: string, b: number }> = string | number
type Values<T> = T[keyof T]

// TS won't believe the result will always have a field "foo"
// so we have to check for it with a conditional first
type Unfoo<T> = T extends { foo: any } ? T["foo"] : never

// combine three helpers to get an intersection of all the item types
type IntersectItems<T extends any[]> = Unfoo<Intersect<Values<Foo<T>>>>

type Test = [
    { a: 1 } | { b: 2 },
    { c: 3 },
]

// this is what we wanted
type X = IntersectItems<Test> // { a: 1, c: 3 } | { b: 2, c: 3 }

// this is not what we wanted
type Y = Intersect<Test[number]> // { a: 1, b: 2, c: 3 }
Run Code Online (Sandbox Code Playgroud)

给定示例中的执行是这样的

IntersectItems<[{ a: 1 } | { b: 2 }, { c: 3 }]> =
Unfoo<Intersect<Values<Foo<[{ a: 1 } | { b: 2 }, { c: 3 }]>>>> =
Unfoo<Intersect<Values<{0: { foo: { a: 1 } | { b: 2 } }, 1: { foo: { c: 3 } }}>>> =
Unfoo<Intersect<{ foo: { a: 1 } | { b: 2 } } | { foo: { c: 3 } }>> =
Unfoo<(({ foo: { a: 1 } | { b: 2 } } | { foo: { c: 3 } }) extends any ? ((x: T) => 0) : never) extends ((x: infer R) => 0) ? R : never> =
Unfoo<(({ foo: { a: 1 } | { b: 2 } } extends any ? ((x: T) => 0) : never) | ({ foo: { c: 3 } } extends any ? ((x: T) => 0) : never)) extends ((x: infer R) => 0) ? R : never> =
Unfoo<(((x: { foo: { a: 1 } | { b: 2 } }) => 0) | ((x: { foo: { c: 3 } }) => 0)) extends ((x: infer R) => 0) ? R : never> =
Unfoo<{ foo: { a: 1 } | { b: 2 } } & { foo: { c: 3 } }> =
({ foo: { a: 1 } | { b: 2 } } & { foo: { c: 3 } })["foo"] =
({ a: 1 } | { b: 2 }) & { c: 3 } =
{ a: 1 } & { c: 3 } | { b: 2 } & { c: 3 }
Run Code Online (Sandbox Code Playgroud)

希望这也展示了一些其他有用的技术。


ade*_*hox 7

jcalz 对这个问题的解决方案[一如既往]完美,但是,对于那些可能不完全理解它的人,这里对他的回答中的这句话有更多解释:

\n
\n

逆变位置中同一类型变量的多个候选者会导致推断出交集类型。

\n
\n

上面的技巧实际上是类型定义中使用的主要技巧UnionToIntersection,但是,术语“逆变”对我来说很模糊,谷歌搜索也没有给出有用的结果,所以让我们在这里回顾一下:

\n

根据维基百科:

\n
\n

许多编程语言类型系统都支持子类型。方差是指更复杂类型之间的子类型与其组件之间的子类型之间的关系。例如,猫列表应如何与动物列表相关?或者返回 Cat 的函数应如何与返回 Animal 的函数相关?

\n
\n

我们看一下上面的代码解释:

\n
type A = (a: Animal) => void\ntype C = (d: Cat) => void\ndeclare let aFunc: A;\ndeclare let cFunc: C;\n
Run Code Online (Sandbox Code Playgroud)\n

现在,您认为以下哪项作业是正确的?

\n
cFunc = aFunc //  [ ]  \xe2\x9c\x85 [ ]\naFunc = cFunc //  [ ]  \xe2\x9c\x85 [ ]\n
Run Code Online (Sandbox Code Playgroud)\n

首先进行猜测,然后继续阅读。:)

\n

与我们对“继承”的期望相反,在“继承”中,子类型的值可以分配给其超类型的变量,但当该变量是函数类型的参数时,这在同一方向上是不正确的,并且我们正在分配“函数”(维基百科的原意是“复杂类型”)。而且有趣的是,它的反方向确实是正确的!即,cFunc = aFunc上面代码片段中的 是正确的,也是aFunc = cFunc错误的。

\n

现在让我们看看为什么cFunc = aFunc是正确的。它正确的原因是,当我们将 Y 类型的某个变量分配给 X 类型的某个变量时,只有当新类型(本例中为 Y)不破坏旧类型的任何用法时,它才是“正确的”输入(本例中为 X)。例如:

\n
a = new Animal()\nc = new Cat()\na = c   // \xe2\x9c\x85 Not breaking, everywhere an Animal is used, a Cat must be useable too\n        // (It is also formally known as the "Liskov Substitution Principle".)\na.eat() // ---> c.eat() \xe2\x9c\x85 No problem, Cats can eat too\n
Run Code Online (Sandbox Code Playgroud)\n

现在,在函数类型的情况下使用相同的规则:如果您将foo函数类型的函数分配给函数类型的Foo变量,那么无论您使用什么,它都必须保持可用/有效。barBarbar

\n
declare let foo: (param: Animal): void\ndeclare let bar: (param: Cat): void\na = new Animal()\nc = new Cat()\n\n// valid usages of foo:\nfoo(a) // \xe2\x9c\x85\nfoo(c) // \xe2\x9c\x85\n\n// valid usage of bar:\nbar(c) // \xe2\x9c\x85\n\nfoo = bar // \xe2\x9d\x8c wrong because \nfoo(a)    // \xe2\x9d\x8c this one has not remained useable / valid\n          // because foo expects a Cat now, but is receiving an Animal, which is not valid\nfoo(c)    // \xe2\x9c\x85\n\nbar = foo // \xe2\x9c\x85 correct because  all usages of bar remains still useable / valid\nbar(c)    // bar expects an Animal now, and has received a Cat, which is still valid\n          // \xe2\xad\x90 That's why we say function parameter is a **contra-variant** position for\n          // a type, because it reverses the direction of the assignability.\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们可以明白为什么cFunc = aFunc是正确的选择了!

\n

有趣的边缘情况是,您可以输入一个函数参数 as ,never这允许您将该参数的任何类型分配给它:

\n
type Foo = (a: never) => void\ntype Bar = (a: Function) => void\ntype Baz = (a: boolean) => void\ntype Qux = (a: SuperComplexType) => void\ndeclare let foo: Foo\ndeclare let bar: Bar\ndeclare let baz: Baz\ndeclare let qux: Qux\nfoo = bar // \xe2\x9c\x85\nfoo = baz // \xe2\x9c\x85\nfoo = qux // \xe2\x9c\x85\n
Run Code Online (Sandbox Code Playgroud)\n

使用相同的猫和动物示例的所有三个 co/contra/in 方差的摘要是:

\n
    \n
  • 协方差() => Cat可分配给() => Animal,因为 Cat 可分配给 Animal ;它“保留了可分配性的方向”。
  • \n
  • 逆变(Animal) => void可分配给(Cat) => void,因为需要动物的东西也可以接受猫;它“逆转了可分配性的方向”。
  • \n
  • 不变性(Animal) => Animal不可分配给(Cat) => Cat,因为并非所有返回的 Animal 都是猫,并且(Cat) => Cat不可分配给 (Animal) => Animal,因为期待 Cat 的东西不能接受任何其他类型的 Animal。
  • \n
\n

现在这就是 jcalz'UnionToIntersection工作原理:

\n
type FirstHalfOfUnionToIntersection<U> = U extends any ? (k: U)=>void : never\n
Run Code Online (Sandbox Code Playgroud)\n

U这是一个分布式条件条件(因为前面的类型extends是裸类型(单独出现并且不是某些更复杂类型表达式的一部分)),因此为联合的每个组件运行条件,例如,在 的情况下,X | Y | Z它产生((k: X) => void) | ((k: Y) => void) | ((k: Z) => void).

\n

在该类型的后半部分,它实际上是这样做的:

\n
<A_union_of_some_functions_from_first_half> extends ((k: infer I)=>void) ? I : never\n
Run Code Online (Sandbox Code Playgroud)\n

这又是一个分布式条件,但是,这是有趣的部分:正在推断的类型I处于逆变位置(它是一个函数参数),因此它的所有可能的推断都将被相交!

\n
\n

逆变位置中同一类型变量的多个候选者会导致推断出交集类型。

\n
\n

例如,继续相同的X | Y | Z示例,结果将是X & Y & Z

\n


bdw*_*ain 5

我稍微扩展了 @jcalz 的答案,以解决他描述的布尔问题。

type UnionToIntersectionHelper<U> = (
  U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

type UnionToIntersection<U> = boolean extends U
  ? UnionToIntersectionHelper<Exclude<U, boolean>> & boolean
  : UnionToIntersectionHelper<U>;
Run Code Online (Sandbox Code Playgroud)

这基本上可以防止它将 under the hood 转换true | false为 a true & false,从而保留boolean其本质。

现在它会正确地说UnionToIntersection<boolean>is boolean, not never,同时仍然正确地说UnionToIntersection<boolean | string>isnever