Transformar el tipo de unión en tipo de intersección

88

¿Hay alguna forma de transformar un tipo de unión en un tipo de intersección?

type FunctionUnion = () => void | (p: string) => void
type FunctionIntersection = () => void & (p: string) => void

Me gustaría aplicar una transformación FunctionUnionpara obtenerFunctionIntersection

Tiziano Cernicova-Dragomir
fuente

Respuestas:

193

¿Quieres unión a la intersección? Los tipos condicionales distributivos y la inferencia de tipos condicionales pueden hacer eso. (No creo que sea posible hacer una intersección a una unión, lo siento) Aquí está la magia malvada:

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

Eso distribuye el sindicato Uy lo vuelve a empaquetar en un nuevo sindicato donde todos los integrantes están en posición contraria. Eso permite inferir el tipo como una intersección I, como se menciona en el manual:

Asimismo, múltiples candidatos para el mismo tipo de variable en posiciones contravariantes hacen que se infiera un tipo de intersección.


Veamos si funciona.

Primero déjeme poner entre paréntesis su FunctionUniony FunctionIntersectionporque TypeScript parece unir la unión / intersección con más fuerza que el retorno de la función:

type FunctionUnion = (() => void) | ((p: string) => void);
type FunctionIntersection = (() => void) & ((p: string) => void);

Pruebas:

type SynthesizedFunctionIntersection = UnionToIntersection<FunctionUnion>
// inspects as 
// type SynthesizedFunctionIntersection = (() => void) & ((p: string) => void)

¡Se ve bien!

Tenga cuidado de que, en general, UnionToIntersection<>exponga algunos detalles de lo que TypeScript piensa que es una unión real. Por ejemplo, booleanaparentemente se representa internamente como true | false, por lo que

type Weird = UnionToIntersection<string | number | boolean>

se convierte en

type Weird = string & number & true & false

que en TS3.6 + se reduce ansiosamente a

type Weird = never

porque es imposible tener un valor que sea string y number y true y false .

Espero que ayude. ¡Buena suerte!

jcalz
fuente
14
10x. Siempre aprendo cosas nuevas e interesantes de ti. Estuve muy cerca de esta pregunta stackoverflow.com/questions/50369299/… pero realmente necesito una forma de transformar la unión en una intersección
Tiziano Cernicova-Dragomir
2
Esta respuesta es asombrosa, pero realmente me resulta difícil entender cómo funciona esta parte "que distribuye la unión U y la vuelve a empaquetar en una nueva unión donde todos los componentes están en posición contravariante " :( No puedo comprender completamente esta parte de la posición contravariante .. Pensé que este código: type Param<T> = T extends (arg: infer U) => void ? U : never; type InferredParams = Param<((a: string) => void) | ((a: number) => void)>;debería darme string & numberpero me da string | number. ¿Puedes explicar por qué?
Mariusz Pawelski
10
Esto se debe a que los parámetros de tipo simple antes extendsde un tipo condicional se distribuyen entre los constituyentes de la unión. Si desea desactivar los tipos condicionales distribuidos, puede utilizar el truco de hacer que el parámetro de tipo "vestida", tales como una tupla de un solo elemento como esto: type Param<T> = [T] extends [(arg: infer U) => void] ? U : never;. Eso debería funcionar de la manera que desee.
jcalz
3
@RanLottem la clave son los tipos condicionales distributivos . El manual lo explica bastante bien, en mi opinión. Lo he ampliado en otros lugares , necesita más información. ¡Buena suerte!
jcalz
1
@NicholasCuthbert ten cuidado; unknownes el resultado "correcto". Para todos Ay B, UTI<A | B>debería ser igual UTI<A> & UTI<B>, ¿no? Vamos a Bser nevery la nota A | neveres A. Ahora tienes UTI<A> = UTI<A> & UTI<never>para todos A. La única forma razonable de satisfacer eso es UTI<never>= unknown, ya que X & unknownes igual a X. Si lo UTI<never>fuera never, entonces UTI<A> = UTI<A> & never, pero desde X & never= never, estás diciendo UTI<A> = neverpara todos A... no es genial. Puede haber casos de uso para UTI<never>ser never, pero no es evidente para mí, así que tenga cuidado.
jcalz
6

También existe un problema muy relacionado cuando desea una intersección de varios tipos, pero no necesariamente convierte uniones en intersecciones. ¡No hay forma de llegar directamente a las intersecciones sin recurrir a uniones temporales!

El problema es que los tipos de los que nos gustaría obtener una intersección podrían tener uniones adentro , que también se convertirán en intersecciones. Guardias al rescate:

// 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 }

La ejecución en el ejemplo dado es así

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 }

Con suerte, esto también muestra algunas otras técnicas útiles.

polkovnikov.ph
fuente