186 votes

Transformer le type union en type intersection

Existe-t-il un moyen de transformer un type d'union en un type d'intersection ?

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

Je voudrais appliquer une transformation à FunctionUnion pour obtenir FunctionIntersection

448voto

jcalz Points 30410

Vous voulez que l'union fasse la jonction ? Types de conditionnels distributifs et inférence à partir de types conditionnels peut le faire. (Je ne pense pas qu'il soit possible de faire l'intersection-union, désolé) Voici la magie maléfique :

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

qui distribue l'union U et le reconditionne dans une nouvelle union où tous les consituants sont en position contravariante. Cela permet de déduire le type comme une intersection I comme indiqué dans le manuel :

De même, si plusieurs candidats pour la même variable de type se trouvent dans des positions contra-variantes, un type d'intersection est déduit.


Voyons si ça marche.

D'abord, laissez-moi mettre entre parenthèses votre FunctionUnion et FunctionIntersection car TypeScript semble lier l'union/intersection plus étroitement que le retour de fonction :

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

Test :

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

Ça a l'air bon !

Attention, en général UnionToIntersection<> expose certains détails de ce que TypeScript pense être une union réelle. Par exemple, boolean est apparemment représenté en interne comme true | false donc

type Weird = UnionToIntersection<string | number | boolean>

devient

type Weird = string & number & true & false

J'espère que cela vous aidera. Bonne chance !

31 votes

10x. J'apprends toujours des choses nouvelles et intéressantes avec vous. J'étais très proche de cette question stackoverflow.com/questions/50369299/ mais j'ai vraiment besoin d'un moyen de transformer l'union en une intersection

6 votes

Cette réponse est géniale mais j'ai vraiment du mal à comprendre comment cette partie "Qui distribue l'union U et la reconditionne en une nouvelle union où tous les constituants sont en position contravariante " fonctionne :( Je n'arrive pas à comprendre tout cela. position contravariante partie. Je pensais que ce code : type Param<T> = T extends (arg: infer U) => void ? U : never; type InferredParams = Param<((a: string) => void) | ((a: number) => void)>; devrait me donner string & number mais ça me donne string | number . Pouvez-vous expliquer pourquoi ?

0 votes

Je pensais que je comprendrais cette réponse en la "divisant". Comme ça : type UnionToIntersectionPart1<U> = U extends any ? (k: U) => void : never; type UnionToIntersectionPart2<U> = U extends ((k: infer I) => void) ? I : never; type IntersectionTest = UnionToIntersectionPart2< UnionToIntersectionPart1<(() => void) | ((p: string) => void)> >; Mais ensuite, ça ne marche pas ! IntersectionTest est toujours une union de types de fonctions, et non une intersection.

17voto

polkovnikov.ph Points 1041

Il existe également un problème très connexe lorsque vous souhaitez obtenir une intersection de plusieurs types, mais sans nécessairement convertir les unions en intersections. Il n'y a tout simplement aucun moyen d'accéder directement aux intersections sans recourir à des unions temporaires !

Le problème est que les types dont nous souhaitons obtenir une intersection peuvent contenir des unions, qui seront également converties en intersections. Les gardes à la rescousse :

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

L'exécution dans l'exemple donné se déroule comme suit

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 }

J'espère que cela montre également d'autres techniques utiles.

6voto

bdwain Points 690

J'ai légèrement étendu la réponse de @jcalz pour contourner le problème booléen qu'il a décrit.

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>;

cela l'empêche essentiellement de convertir le true | false sous le capot à un true & false en préservant la boolean la nature de celle-ci.

Maintenant, il dira correctement UnionToIntersection<boolean> es boolean pas never tout en disant correctement UnionToIntersection<boolean | string> es never

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X