Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Future proof union to intersection type conversion #29594

Open
5 tasks done
zenorbi opened this issue Jan 25, 2019 · 43 comments
Open
5 tasks done

Future proof union to intersection type conversion #29594

zenorbi opened this issue Jan 25, 2019 · 43 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@zenorbi
Copy link

zenorbi commented Jan 25, 2019

Search Terms

union to intersection, type merge

Suggestion

I'd like to either standardize (meaning documented behavior) or have a less hacky (future proof) way of transforming union types to intersections.

Use Cases

The most common use case I can think of is a basic action system where each action has some kind of input data it can work with to determine its disabled status. An actionGroup which packs multiple actions into one updater function needs to request the right input data type which is compatible to all of its actions.

While it's possible in the current version of typescript, it feels like a hack.

Examples

A more presentable form by describing a car dashboard:

interface Gauge<T> {
    display(data: T): void;
}

class SpeedGauge implements Gauge<{ speed: number; }> {
    display(data: { speed: number; }) {
    }
}

class TempGauge implements Gauge<{ temperature: number; }> {
    display(data: { temperature: number; }) {
    }
}

class RevGauge implements Gauge<{ rev: number; }> {
    display(data: { rev: number; }) {
    }
}

type ExtractGaugeType<T> = T extends Gauge<infer U> ? U : never;

// evil magic by jcalz https://stackoverflow.com/a/50375286
// I would like to future proof or have a better version of this
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

class Dashboard<T extends Gauge<unknown>> {
    constructor(public gauges: T[]) {
    }

    display(data: UnionToIntersection<ExtractGaugeType<T>>) {
        this.gauges.forEach((g) => g.display(data));
    }
}

const myDashboard = new Dashboard([new SpeedGauge(), new TempGauge(), new RevGauge()]);

/*
the type is: { rev: number; } & { speed: number; } & { temperature: number; }
*/
myDashboard.display({ // Ok
    speed: 50,
    rev: 2000,
    temperature: 85
});

myDashboard.display({ // Error: property "rev" is missing
    speed: 50,
    temperature: 85
});

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@AnyhowStep
Copy link
Contributor

Damn. I wish I knew about this trick sooner!
I've been using tuples, conditional types, and copy-pasting code to get unions to become intersections!

@jack-williams
Copy link
Collaborator

jack-williams commented Jan 26, 2019

If negated types are merged I think you can do this:

type UnionToInterWorker<T> = not (T extends T ? not T : never);
type UnionToInter<T> = [T] extends [never] ? never : UnionToInterWorker<T>;

Though I think we need to be careful about using complex type operations as a sledgehammer. The more complex the type operators the greater that disparity between our code and types, and we end up having to rely on casts to get things through.

First, IMO, is to look at what we are trying to solve (semantically, not mechanically) and try and get the type system to work with us. With @weswigham's work on union signatures I can get your example to type-check with the following:

class Dashboard<T extends Gauge<unknown>> {
    constructor(public gauges: T[]) {
        
    }

    display: T["display"] = (data) => {        
        this.gauges.forEach((g) => g.display(data));
    }
}

myDashboard.display({ // Ok
    speed: 50,
    rev: 2000,
    temperature: 85
});

/**
 * Argument of type '{ speed: number; temperature: number; }' is not assignable to parameter of type '{ speed: number; } & { rev: number; } & { temperature: number; }'.
 * Property 'rev' is missing in type '{ speed: number; temperature: number; }' but required in type '{ rev: number; }'. [2345]
 */
myDashboard.display({
    speed: 50,
    temperature: 85
});

Not only is this clearer, but it precisely captures intent. To me, at least, this is exactly the kind of solution TypeScript should be encouraging, and I think adding a load of complex type manipulation as part of the standard would guide people in the wrong direction.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jan 26, 2019

What about this use case (adapted from a personal project),

function and<ArrT extends AnonymousTypedExpr<boolean>[]> (...arr : ArrT) : Expr<{
    usedRef : UnionToIntersection<ArrT[number]["usedRef"]>,
    value : boolean,
}>;

usedRef' is a type such that I can get away with using the & operator instead of using complicated type merging operations.

But without the magical UnionToIntersection, I'm left with either copy pasting and creating overloads for each number of arguments, or using conditional types for each length of a tuple

@zenorbi
Copy link
Author

zenorbi commented Jan 26, 2019

@jack-williams I can't seem to make your example work. Using TypeScript@3.2.4 and it tells me

Dashboard<SpeedGauge | TempGauge | RevGauge>.display: ((data: {
    speed: number;
}) => void) | ((data: {
    rev: number;
}) => void) | ((data: {
    temperature: number;
}) => void)

and

[ts] Cannot invoke an expression whose type lacks a call signature. Type '((data: { speed: number; }) => void) | ((data: { rev: number; }) => void) | ((data: { temperature: number; }) => void)' has no compatible call signatures. [2349]

myDashboard.display({
    speed: 50,
    rev: 2000,
    temperature: 85
});

@jack-williams
Copy link
Collaborator

@zenorbi The feature is set for 3.3. Release notes.

@zenorbi
Copy link
Author

zenorbi commented Jan 26, 2019

@jack-williams Oh, this is a nice surprise. And I also agree with you that your T["display"] solution is much closer to what to code actually does. Thank you for your help.

@zenorbi zenorbi closed this as completed Jan 26, 2019
@jack-williams
Copy link
Collaborator

@zenorbi

No problem! Yes, it is a really nice addition to the language.

I will just add the caveat that I have nothing to do with TS team, so please don't take my view as anything more than an outsider perspective.

I do think there is a discussion to be had around what type operators are needed in the language: a union to intersection operator might be one of them. If the problems people face can be solved by being smarter with existing types then I think that is better, but sometimes things really do need new features.

Unless your issue was really about that specific use case, then it might be worth leaving the issue open and getting the POV from the team and other users.

@zenorbi zenorbi reopened this Jan 26, 2019
@RyanCavanaugh RyanCavanaugh added Discussion Issues which may not have code impact Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed Discussion Issues which may not have code impact labels Feb 5, 2019
@RyanCavanaugh
Copy link
Member

I don't think we can commit to any particular "black magic" working in the future, nor offer a built-in operation for it unless more common use cases arise.

@weswigham
Copy link
Member

The negated type method is likely pretty solid, though. It falls out from a core identity of the type, so once merged, it's highly unlikely to cease to work. IMO, it'll be more of a constant than the variance-reliant inversion you can find in the wild today.

@jack-williams
Copy link
Collaborator

Agree with @weswigham that the negated approach seems reliable; if it did not work that would probably suggest something is not quite right with the implementation of negated, intersection, and union types.

I guess an open question might be whether such a type should be part of the standard lib like Exclude. IMO I don't think it should be. I'm not sure what behaviour people would expect on types like never, unknown, and any----making the right call there seems hard.

Maybe a consquence of this issue might be some test cases for the negated type PR? It's not going so far as to directly support the operator, but it would at least be on the radar if something affected it.

@sledorze
Copy link

I've seen developers wasting hours (re)doing that very transform on different projects (with hacks that can break at every single release).

The amount of knowledge today to be able to write or even just understand how to achieve this pattern can only be acquired by doing type level typescript for weeks or even months, not the freshman.

Having an explicitly named and maintained type operator UnionToIntersection (whatever the implementation) would be a real life safer!

(If you add it, you can literally save lives!)

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Mar 27, 2019

@sledorze There are all sorts of hacks you can do in the TS type system that are likely to break, but jcalz's version of UnionToIntersection has been the same since he posted it almost a year ago. I have used it regularly without any issues.

Is it a bit difficult to understand for the uninitiated? Probably, but most advanced TS features can feel a bit like dark magic. Does it rely un undocumented behavior? No. The behavior is all straight from the docs.

It relies on the distributive behavior of conditional types (in this part (U extends any ? (k: U) => void : never), although U extends unknow might be better today):

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation.

And the behavior of infer in relation to type variables in contra-variant positions (in this part (...) extends ((k: infer I) => void) ? I : never;):

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred: [...]

This is all documented in the official docs and thus is not likely to break.

Could someone please articulate the exact issue with UnionToIntersection ? I am genuinely curios what problems people have with it as it is currently written. (Except for it being hard to understand which I will freely concede)

BTW: I would definitely vote to include it in lib.d.ts but maybe it is too specialized a type to include there.

@jack-williams
Copy link
Collaborator

jack-williams commented Mar 27, 2019

I've seen very few real-word examples where an explicit union-to-intersection type operation is the natural thing to do. Those that are wanting to have it 'officially' supported or added to the lib should at least offer some examples that benefit from the type.

My concern with these kinds of types is that they encourage arbitrarily complex signatures that library functions cannot reasonably implement, and TypeScript cannot realistically check against. We end up in a situation where people are maintaining distinct run-time and type logic, plastered together using any and assertions.

@sledorze
Copy link

You have it from @gcanti link above.
The linked issue has a link to real world lib

@AnyhowStep
Copy link
Contributor

AnyhowStep commented May 25, 2019

Array.fill<>() can benefit from UnionToIntersection<>.

//== Problem ==
const t: [1, 2] = [1, 2];
//Expected: Compile Error
//Actual: OK
t.fill(1);

//[1, 1]
console.log(t);

//== Suggestion ==
/*
    Introduce a UnionToIntersection<> helper type,
    or introduce some type-level operation for it.
*/
export type UnionToIntersection<U> = (
    (
        U extends any ? (k: U) => void : never
    ) extends (
        (k: infer I) => void
    ) ? I : never
);

declare function fill<ArrT extends any[]>(
    arr: ArrT,
    value: UnionToIntersection<ArrT[number]>
): void;

//Expected: Compile Error
//Actual: Argument of type '1' is not assignable to parameter of type '1 & 2'.
fill(t, 1);

Playground

In general, I've found that projects where I handle arrays, tuples, and union types tend to need UnionToIntersection<> at some point, especially if a complicated system is being modeled to be checked at compile-time.


In particular, right now, I have a data mapping project where I have functions like this,

type SafeTypeMapDelegate<ReturnT> = (name : string, mixed : unknown) => ReturnT;

All the mapping functions should handle all kinds of unknown values properly.

However, there are types that they "prefer" to receive, which "guarantee" the mapping will succeed.
I add these as a kind of brand,

type Accepts<AcceptT> = { __accepts? : [AcceptT] };

Given two SafeTypeMapDelegate<> types, F and G, I want to know what F|G "prefers" to receive to guarantee the mapping will succeed.

A naive approach does not work,

type AcceptsOf<T> = T extends Accepts<infer AcceptT> ? AcceptT : ReturnType<T>;
//AcceptsOf<F|G> will give AcceptsOf<F>|AcceptsOf<G>

We want AcceptsOf<F> & AcceptsOf<G>. My current solution,

type AcceptsOfInner<T> = T extends Accepts<infer AcceptT> ? [AcceptT] : [ReturnType<T>];
type AcceptsOf<T> = Extract<UnionToIntersection<AcceptsOfInner<T>, [any]>>[0];
//AcceptsOf<F|G> is now AcceptsOf<F> & AcceptsOf<G> and will work for unions of any length

@nandin-borjigin
Copy link

nandin-borjigin commented Jul 2, 2019

Edit

This approach is considered to be:

  • verbose
  • of impact on compiler performance
  • subject to future changes

Original

There has been several cases where I need to convert a union type into intersections. And I realized that the union types are all extracted from an array (as in the OP's example code, Dashboard constructor is called with an array of type (SpeedGauge | TempGauge | RevGauge)[] and generic argument T is inferred as SpeedGauge | TempGauge | RevGauge).

While the magical UnionToIntersection works as expected, it's too hacky and the concept of Variance is so obscure. Fortunately, There is another solution to this particular scenario (unions inferred from generic arrays).

playground link

/** Type helpers */
type Prepend<T, Arr extends any[]> = ((t: T, ...a: Arr) => void) extends ((...args: infer U) => void) ? U : Arr
type ExtractGaugeType<T> = T extends Gauge<infer U> ? U : never;

type MergeGaugesData<
    /** type parameters */
    Gauges extends Gauge<any>[],
    /** internals */
    /** resulting type */ _R = {},
    /** iterator */       _I extends any[] = [],
    /** local variable */ _aGauge = Gauges[_I['length']],
    /** local variable */ _data = ExtractGaugeType<_aGauge>
> = {
    next: MergeGaugesData<Gauges, _R & _data, Prepend<any, _I>>
    done: _R
}[
    // If the iterator is at the end of the input array
    _I['length'] extends Gauges['length']
        // then
        ? 'done'
        // else
        : 'next'
]
/**************************** */



/** Usage */
// changing interface to abstract class here to reduce verbosity in subclasses
abstract class Gauge<T> {
    display(data: T) {} 
}

class SpeedGauge extends Gauge<{ speed: number; }> {}
class TempGauge extends Gauge<{ temperature: number; }> {}
class RevGauge extends Gauge<{ rev: number; }> {}


class Dashboard<Gauges extends Gauge<any>[]> {
    public gauges: Gauges
    // rest argument is required for inferring a tuple type
    constructor(...gauges: Gauges) {
        this.gauges = gauges
    }

    display(data: MergeGaugesData<Gauges>) {
        this.gauges.forEach((g) => g.display(data));
    }
}

const myDashboard = new Dashboard(new SpeedGauge(), new TempGauge(), new RevGauge());


/*
the type is: { rev: number; } & { speed: number; } & { temperature: number; }
*/
myDashboard.display({ // Ok
    speed: 50,
    rev: 2000,
    temperature: 85
});

myDashboard.display({ // Error: property "rev" is missing
    speed: 50,
    temperature: 85
});

/********* */

It might look frightening at first glance, but is actually quite straightforward. MergeGaugeData is just like a simple iteration any programmer would write -- given an initialized state value and an array, modify the state value according to each array element at each iteration step. The only difference is this iteration takes place in type declaration space.

In case of MergeGaugeData, Gauges is the array (tuple actually), _R is the state value initialized to {} and _I is the iterator. _aGuage and _data are merely two local variables and can be inclined, if you like. The iterator is actually a tuple and it's length property, which is a number literal, is used as an index. And we increment the iterator by prepending a new element, which in turn increases the length property. The iteration ends when the condition _I['length'] extends Gauges['length'] satisfies.

While MergeGaugeData contains hard coded context specific logic and is not applicable to other kind of arrays, it can be easily generallized as and used on any arbitrary array.

type Prepend<T, Arr extends any[]> = ((t: T, ...a: Arr) => void) extends ((...args: infer U) => void) ? U : Arr
type ArrayToIntersection<
    Arr extends any[],
    // internal
    _R = {},
    _I extends any[] = [],
    _Elm = Arr[_I['length']]
> = {
    next: ArrayToIntersection<Arr, _R & _Elm, Prepend<any, _I>>,
    done: _R
}[
    Arr['length'] extends _I['length'] ? 'done' : 'next'
]

MergeGaugeData needs to be written like that since the components of the intersection to be returned are derivatives of the array elements instead of the elements themselves.

The iteration pattern used here, which can be modified to achieve many powerful typings, was inspired by (stolen from) a post by Pierre-Antoine Mills.

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Jul 2, 2019

@Nandiin
The UnionToIntersection version of this would be

type UnionToIntersection<T> = (T extends T ? ((p: T)=> void): never) extends (p: infer U)=> void ? U: never;
type GetGaugeData<T extends Gauge<any>> = T extends Gauge<infer U> ? U : never

class Dashboard<Gauges extends Gauge<any>[]> {
    public gauges: Gauges
    // rest argument is required to infer a tuple type
    constructor(...gauges: Gauges) {
        this.gauges = gauges
    }

    display(data: UnionToIntersection<GetGaugeData<Gauges[number]>>) {
        this.gauges.forEach((g) => g.display(data));
    }
}

IMO This takes a well understood type transformation (UnionToIntersection) and a 3 line solution and explodes it into a hand crafted recursive type alias that takes 13 lines to write. Not sure if this is an improvement, in my opinion it is not. UnionToIntersection is a simple to understand type transformation (takes a union returns an intersection), you only need understand what it does, not how it does it, at least in 90% of cases.

Also this explicit iteration version will cause a LOT of type instantiations, you might get in trouble with a future version of TS (see #32079)). UnionToIntersection uses intrinsic compiler primitives to achieve the same result without generating a bunch of new types.

Can anyone with a "member" badge (cc: @RyanCavanaugh @weswigham ) comment to this recursive type alias hack. Is it something that should be used freely, should be used sparingly as it has compiler performance implications, or should be avoided like the plague as it might break in the future. I always assumed it was the latter and avoided it in anything except "fun experiments"

@jcalz
Copy link
Contributor

jcalz commented Jul 2, 2019

👀 😲 How did I miss seeing this issue?

I think using the word "hack" to describe any of these implementations might not be productive so I will avoid it. I might have poisoned the well by describing my own implementation

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

as "evil magic", but I didn't mean to imply that the implementation was somehow bending or breaking any rules of the language. That one-line type alias only uses documented and supported features of conditional types, so I think any change to the language that breaks it will probably be a breaking change for everyone. (I assume that @RyanCavanaugh was speaking generally and not specifically saying that this implementation is doing something unuspported; if I'm wrong hopefully he'll smite correct me.)


As for using recursive conditional types to implement this... I think #26980 is still the canonical issue about recursive conditional types, and the current status of that is "don't do this" and "this isn't supported". If anyone knows otherwise, they should go to that issue and speak up.

Even if recursive conditional types were/are/become supported, I doubt they would be a better solution for UnionToIntersection than the one above since I'd expect recursive types to have worse performance than iterative types, all else being equal.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jul 2, 2019

Might as well put an explanation for UnionToIntersection<> here.

type UnionToIntersection<U> = (
    (
        //Step 1
        U extends any ?
            (k : U) => void :
            never
    ) extends (
        //Step 2
        (k : infer I) => void
    ) ?
            I :
            never
);

And we use it like so,

type obj = UnionToIntersection<{ x : 1 }|{ y : 2 }>;

Step 1

In Step 1, we have,

//Where U = { x : 1 }|{ y : 2 }
    (
        //Step 1
        U extends any ?
            (k : U) => void :
            never
    )

This part becomes,

(
    | ((k : { x : 1 }) => void)
    | ((k : { y : 2 }) => void)
)

Playground


If we had just used (k : U) => void, we would have gotten (k : { x : 1 }|{ y : 2 }) => void.
We need the conditional type (U extends any ?) to distribute the union.

(Can someone find a nice link that demonstrates conditional types distributing unions?)


Step 2

In Step 2, we have,

/*Step 1*/ extends ((k : infer I) => void) ?
    I :
    never

Which is just,

//{ x: 1 } & { y: 2 }
type result = (
    | ((k : { x : 1 }) => void)
    | ((k : { y : 2 }) => void)
) extends ((k : infer I) => void) ?
    I :
    never

Playground


Recall that the type in step 1 is,

type unionOfFoo = (
    | ((k : { x : 1 }) => void)
    | ((k : { y : 2 }) => void)
)

Imagine we had,

declare const f : unionOfFoo;

In order to invoke f(/*arg*/), this "arg" must be { x : 1, y : 2 }.

  • We don't know if unionOfFoo is actually (k : { x : 1 }) => void
  • We don't know if unionOfFoo is actually (k : { y : 2 }) => void

So, to safely call unionOfFoo, we have to pass in a type that will satisfy the requirements of both functions. This type happens to be { x : 1, y : 2 }.

One final playground link to play with,

Playground


Conclusion

The above wall of text is a mess. Maybe someone can rewrite it more succinctly.

However, I'm pretty sure that this UnionToIntersection<> will not be breaking any time soon. And it should not.

The Playground links may be helpful in understanding this UnionToIntersection<> type. More helpful than the walls of text, anyway.

@jcalz
Copy link
Contributor

jcalz commented Jul 2, 2019

Can someone find a nice link that demonstrates conditional types distributing unions?

Does the handbook count?

@nandin-borjigin
Copy link

Thanks to @AnyhowStep for the detailed explaination and also @jcalz and @dragomirtitian for the thoughtful comments.

My last usecase needing a union-to-intersection conversion requires recursive type regardless. And it might be the reason why I thought recursive type would be nicer, as there was already a recursion at the moment.

However, I'm pretty convinced that the former approach is better than the recursive one after reading your comments and some other resources. And I also edited the original comment to highlight the shortages it has.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Aug 11, 2019

Another use case for union to intersection,

query
  .whereEqPrimaryKey(
    tables => Math.random() > 0.5 ? tables.tableA : tables.tableB,
    {
      tableAId : 1337,
      tableBId : 9001,
    }
  );

The PrimaryKeyT type of a TableT type depends on whether TableT is a union or not, and whether PrimaryKeyT is being used as an input (function param) or output (result of function).

In the above example, { tableAId : number } is the PK of tableA, { tableBId : number } is the PK of tableB.

When the input TableT is a union type, the input PrimaryKeyT must be the UnionToIntersection of each table's PK.

When the input TableT is a union type, the output PrimaryKeyT is the union of each table's PK.


You can apply the above logic for candidate keys and super keys, too.

Except, for candidate keys, it becomes a union of intersection types (only one PK, but multiple CKs).

For super keys, it becomes a union of intersection types and partial types (CK props are required. Non-CK props are optional)

@fabb
Copy link

fabb commented Aug 24, 2019

Here‘s a very good blog post on a use case of UnionToIntersection in React, and its problematic performance implications:
https://blog.andrewbran.ch/polymorphic-react-components/

@dontsave
Copy link

dontsave commented Oct 2, 2019

Can someone explain to me why

type Test = ((t: 'a') => void) | ((t: 'b') => void) extends ((t: infer U) => void) ? U : never

Test has type "a" & "b" but

type Test1<T> = T extends ((t: infer U) => void) ? U : never
type Test2 = Test1<((t: 'a') => void) | ((t: 'b') => void)>

Test2 has type "a" | "b" ?

@weswigham
Copy link
Member

weswigham commented Oct 2, 2019

The latter is a distributive conditional (so it's templated over each input union member and then joined into a union), the former is not (so follows variance based inference rules for combining inferences at each position).

Essentially the first is trying to find a result that holds for the whole thing, while the second is forming an individual result for each member of the input union and then combining them into a new union.
type Test1<T> = [T] extends [((t: infer U) => void)] ? U : never should disable distributive behaviors and behave the same way as the first.

@maclockard
Copy link

maclockard commented Oct 11, 2019

Here's a powerful use case for this feature that we are currently using out in the wild:

type Table<T> = {
  select(): T[];
  join<J extends Table<any>[]>(...tables: J): Array<T & UnionToIntersection<{ [K in keyof J]: J[K] extends Table<infer T2> ? T2 : never }[number]>>;
}

declare const personTable: Table<{ firstName: string; lastName: string }>;

declare const addressTable: Table<{ address: string }>;

declare const jobTable: Table<{ job: string }>;

// The type here resolves to:
// { 
//   firstName: string;
//   lastName: string;
//   address: string;
//   job: string;
// }[]
const resolvePersonTable = personTable.join(addressTable, jobTable);

working playground

This has been somewhat simplified to make it easier to follow, but what this allows you to do is have type safe joins assuming the types are correctly declared on the underlying tables. This is a pretty powerful use of typescript. By using some fairly complex typing features you end up with an API that's easier to understand and use.

Making sure that UnionToIntersection doesn't break or possibly adding it as a first class feature would make writing similar features easier and more common place.

@dontsave
Copy link

The following

type UnionToIntersection<T> = (T extends T ? ((p: T) => void) : never) extends (p: infer U) => void ? U : never

type A = 'a' | 'b'

type Test = UnionToIntersection<A>

in TS 3.5.1 (playground) gives Test the type 'a' & 'b'

however in TS 3.6.3 (playground) it has a never type?

@weswigham
Copy link
Member

'a' & 'b' is equivalent to never, as such a type has no sasifying values other than the empty set. In 3.5, we only did that reduction when such an intersection was placed into a union, in 3.6 and beyond, we're slightly more eager with when we reduce the intersection to never.

@AnyhowStep
Copy link
Contributor

/* @internal */
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

At this point, we might as well make it part of the lib.d.ts file as a helper type =P

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 2, 2019

Anyway, it would be nice if we could fix some of the problems UnionToIntersection<> has.
Example,

type a = { x : ("a" | "b" | "c" | "d")[] }
type b = { x : ("c" | "d" | "e" | "f")[] }
type UnionToIntersection<U> =
    (
        U extends any?
        (arg: U) => void :
        never
    ) extends (
        (arg : infer I) => void
    )?
    I :
    never
;

//OK
type g = (a["x"] | b["x"])[number]
//OK
type g2 = UnionToIntersection<a["x"] | b["x"]>[number]
//OK
type g3 = (a["x"] & b["x"])[number]

//OK
type G<A extends { x: string[] }, B extends { x: string[] }> =
    (A["x"] | B["x"])[number]
;
//Expected: OK
//Actual  : Error
type G2<A extends { x: string[] }, B extends { x: string[] }> =
    UnionToIntersection<A["x"] | B["x"]>[number]
;
//OK
type G3<A extends { x: string[] }, B extends { x: string[] }> =
    (A["x"] & B["x"])[number]
;

Playground


My current workaround is to use Extract<>. But I don't like it because it's basically the same as a type-level as operator.

//OK; but requires `Extract<>` workaround
type G2<A extends { x: string[] }, B extends { x: string[] }> =
    Extract<UnionToIntersection<A["x"] | B["x"]>, string[]>[number]
;

Playground


Smaller repro here: Playground


Another workaround,

export type UnionToIntersection2<
    U extends BaseT,
    BaseT
> =
    Extract<
        UnionToIntersection<U>,
        BaseT
    >
;
//OK, but requires workaround
type G2<A extends { x: string[] }, B extends { x: string[] }> =
    UnionToIntersection2<A["x"] | B["x"], string[]>[number]
;

@c-vetter
Copy link

@AnyhowStep
What is your fill example supposed to accomplish that wouldn't be better served by const t = [1, 2] as const? Additionally, what is your version of fill supposed to do? After all, type 1&2 cannot be fulfilled.

The SafeTypeMapDelegate example seems incomplete. How would AcceptsOf<F> & AcceptsOf<G> be used? Also, how does it give you the guaranteed-to-be-good type you want?

The whereEqPrimaryKey example lacks concrete typing information. That makes it impossible to accurately grasp the real use it would derives from any conversion.

Regarding the problem you point out with your use of UnionToIntersection in type G2, you can actually fix that without casting by asserting the type:

type G2<A extends string[], B extends string[]> =
    UnionToIntersection<A | B> extends string[] ? UnionToIntersection<A | B>[number] : never
;

@c-vetter
Copy link

@fabb
That blog post uses UnionToIntersection only as a stepping stone on its way to decoupling the rendering of its child wrapper, so it does not show a real use-case.

@c-vetter
Copy link

@maclockard
While tinkering to understand your use-case, I found that your UnionToIntersection<{ [K in keyof J]: J[K] extends Table<infer T2> ? T2 : never }[number]> can be shortened to UnionToIntersection<(J extends Table<infer U>[] ? U : never)>. Is your way of arriving at the desired type intentionally more verbose? If so, can you explain the reasoning?

@c-vetter
Copy link

@Nandiin

My last usecase needing a union-to-intersection conversion requires recursive type regardless.

How so? @dragomirtitian's Proposal works as he described.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Oct 18, 2020

@rasenplanscher
I'm too lazy to have to defend my use of the type. This issue is very old. Suffice to say that I'm not the only person who uses this type.

It's even in the TS code base.

UTI is essential for typing input positions, when TS does not realize a function/method is intended to behave like a union of functions/methods. (Sort of)

If you're really so eager, https://github.com/AnyhowStep/tsql/tree/master/src

I use UTI in some places above

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Oct 18, 2020

What is your fill example supposed to accomplish that wouldn't be better served by const t = [1, 2] as const?

How the type [1, 2] was derived has nothing to do with this issue. Also, const assertions were not added till a few days after that comment you quoted.

After all, type 1&2 cannot be fulfilled

You walked right into the point

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Oct 18, 2020

@maclockard
While tinkering to understand your use-case, I found that your UnionToIntersection<{ [K in keyof J]: J[K] extends Table<infer T2> ? T2 : never }[number]> can be shortened to UnionToIntersection<(J extends Table<infer U>[] ? U : never)>. Is your way of arriving at the desired type intentionally more verbose? If so, can you explain the reasoning?

You're doing something else completely.

He is operating on properties of J, you're operating on J as a whole.

J is not Table<>.
J is Record<string, Table<>>

@maclockard
Copy link

maclockard commented Oct 18, 2020

@maclockard
While tinkering to understand your use-case, I found that your UnionToIntersection<{ [K in keyof J]: J[K] extends Table<infer T2> ? T2 : never }[number]> can be shortened to UnionToIntersection<(J extends Table<infer U>[] ? U : never)>. Is your way of arriving at the desired type intentionally more verbose? If so, can you explain the reasoning?

Yeah, that works for this use case, what I have there is just what I originally arrived at. Either way, still need UnionToIntersection.

@AnyhowStep it still works in this case since J is a tuple of Table<>s. Its not entirely clear from just reading the type since TS uses the same syntax as Record<string, Table<>> when it comes to mapped types.

@AnyhowStep
Copy link
Contributor

I didn't see the [] part of the type on my phone lol

@c-vetter
Copy link

@AnyhowStep Just to be clear: my messages are in no way meant as an attack.

It's even in the TS code base.

That's true. Unfortunately, I seem to have missed your note to that effect 😕
Now that I'm aware of that, I second your proposal to include it in lib.d.ts.
Regarding that, is there a good reason against including it in its current form?
Looking at types.ts and this thread, it seems safe to say that it is stable.

Regarding the G2 issue above, I don't feel qualified to tell whether
the current behavior is wrong or just inconvenient, much less how to fix it.
Seeing as it can be worked around rather easily, and that UnionToIntersection
generally works as intended, I'm inclined to leave it like that and just publish it as is.

Any contrary indications?

@vogler
Copy link

vogler commented Mar 4, 2021

If the intersection for a field is never, you can't provide a value for the object anymore.
So when using UnionToIntersection on objects out of your control the below StripNever may be useful.

type t = UnionToIntersection<{a: number, b: number} | {a: number, b: string}>;
const t1: t = {a: 1}; //  Property 'b' is missing in type '{ a: number; }' but required in type '{ a: number; b: number; }'
const t2: t = {a: 1, b: 1}; // Type 'number' is not assignable to type 'never'.

type NonNeverKeys<T> = { [K in keyof T]: T[K] extends never ? never : K }[keyof T];
type StripNever<T> = T extends object ? { [K in NonNeverKeys<T>]: StripNever<T[K]> } : T;

const t3: StripNever<t> = {a: 1}; // ok

@SephReed
Copy link

Right now I'm running into:

The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.

So I'm trying to remove every little bit of complexity I can, and this crazy utility type would be very, very nice to have simplified.

@viniciusflv
Copy link

Another contribution to state the importance of this issue

Please tell me if I am missing something

Using the "black magic" solution.

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

This is the simplest scenario that I can think of.

I need a function that receives two objects and returns one, with the correct inference of my arguments.

function join<T extends Record<string, any>[]>(...args: T): UnionToIntersection<T[number]> {
  // Using any due the problems listed below
  return args.reduce<any>((acc, arg) => ({ ...acc, ...arg }), {});
}

const joined = join({ foo: '' }, { bar: '' });

Output type:

const joined: {
    foo: string;
} & {
    bar: string;
}

The problems are:

  • Mismatching between the extended generic type, Record<string, any>, and the inferred arg intersection, UnionToIntersection<T[number]>

    Since T extends Record<string, any> and UnionToIntersection<T[number]> returns T[0] & T[1]...

    Is expected that the output of Union to intersection would be equivalent to Record<string, any>,
    because T[0] & T[1]... is Record<string, any> & Record<string, any>...

    Type 'Record<string, any>' is not assignable to type 'UnionToIntersection<T[number]>'
  • If you force the reducer initial value to correspond to UnionToIntersection<T[number]>

    function join<T extends Record<string, any>[]>(
    ...args: T
    ): UnionToIntersection<T[number]> {
      return args.reduce(
        (acc, arg) => ({ ...acc, ...arg }),
        {} as UnionToIntersection<T[number]>,
      );
    }

    The following error is presented

    (parameter) acc: UnionToIntersection<T[number]>
    Spread types may only be created from object types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests