Open zenorbi opened 5 years ago
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!
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.
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
@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
});
@zenorbi The feature is set for 3.3. Release notes.
@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
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.
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.
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.
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.
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!)
@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.
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.
You have it from @gcanti link above. The linked issue has a link to real world lib
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%20extends%20(%0D%0A%20%20%20%20%20%20%20%20(k%3A%20infer%20I)%20%3D%3E%20void%0D%0A%20%20%20%20)%20%3F%20I%20%3A%20never%0D%0A)%3B%0D%0A%0D%0Adeclare%20function%20fill%3CArrT%20extends%20any%5B%5D%3E(%0D%0A%20%20%20%20arr%3A%20ArrT%2C%0D%0A%20%20%20%20value%3A%20UnionToIntersection%3CArrT%5Bnumber%5D%3E%0D%0A)%3A%20void%3B%0D%0A%0D%0A%2F%2FExpected%3A%20Compile%20Error%0D%0A%2F%2FActual%3A%20Argument%20of%20type%20'1'%20is%20not%20assignable%20to%20parameter%20of%20type%20'1%20%26%202'.%0D%0Afill(t%2C%201)%3B)
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
This approach is considered to be:
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).
/** 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.
@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 https://github.com/microsoft/TypeScript/pull/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"
π π² 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.
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 }>;
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)
)
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?)
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
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 }
.
unionOfFoo
is actually (k : { x : 1 }) => void
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,
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.
Can someone find a nice link that demonstrates conditional types distributing unions?
Does the handbook count?
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.
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)
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/
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"
?
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.
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);
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.
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?
'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
.
At this point, we might as well make it part of the lib.d.ts
file as a helper type =P
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]
;
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]
;
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]
;
@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
;
@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.
@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?
@Nandiin
My last usecase needing a union-to-intersection conversion requires recursive type regardless.
How so? @dragomirtitian's Proposal works as he described.
@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
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
@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 toUnionToIntersection<(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 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 toUnionToIntersection<(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.
I didn't see the []
part of the type on my phone lol
@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?
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
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.
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
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: '' });
const joined: {
foo: string;
} & {
bar: string;
}
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.
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:
Checklist
My suggestion meets these guidelines: