-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
Damn. I wish I knew about this trick sooner! |
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,
}>;
But without the magical |
@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
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 |
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 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 (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 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
And the behavior of
This is all documented in the official docs and thus is not likely to break. Could someone please articulate the exact issue with BTW: I would definitely vote to include it in |
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 |
You have it from @gcanti link above. |
//== 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); In general, I've found that projects where I handle arrays, tuples, and union types tend to need 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 However, there are types that they "prefer" to receive, which "guarantee" the mapping will succeed. type Accepts<AcceptT> = { __accepts? : [AcceptT] }; Given two 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 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 |
EditThis approach is considered to be:
OriginalThere 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, While the magical /** 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. In case of While 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'
]
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 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 ( 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)). 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 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 |
Might as well put an explanation for 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 1In 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 (Can someone find a nice link that demonstrates conditional types distributing unions?) Step 2In 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
So, to safely call One final playground link to play with, ConclusionThe above wall of text is a mess. Maybe someone can rewrite it more succinctly. However, I'm pretty sure that this The Playground links may be helpful in understanding this |
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 In the above example, When the input When the input 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 |
Can someone explain to me why type Test = ((t: 'a') => void) | ((t: 'b') => void) extends ((t: infer U) => void) ? U : never Test has type type Test1<T> = T extends ((t: infer U) => void) ? U : never
type Test2 = Test1<((t: 'a') => void) | ((t: 'b') => void)> Test2 has type |
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. |
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 |
The following
in TS 3.5.1 (playground) gives Test the type 'a' & 'b' however in TS 3.6.3 (playground) it has a never type? |
|
TypeScript/src/compiler/types.ts Lines 6420 to 6422 in d02531f
At this point, we might as well make it part of the |
Anyway, it would be nice if we could fix some of the problems 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 //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 The The Regarding the problem you point out with your use of
|
@fabb |
@maclockard |
How so? @dragomirtitian's Proposal works as he described. |
@rasenplanscher 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 |
How the type
You walked right into the point |
|
Yeah, that works for this use case, what I have there is just what I originally arrived at. Either way, still need @AnyhowStep it still works in this case since |
I didn't see the |
@AnyhowStep Just to be clear: my messages are in no way meant as an attack.
That's true. Unfortunately, I seem to have missed your note to that effect 😕 Regarding the Any contrary indications? |
If the intersection for a field is never, you can't provide a value for the object anymore. 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:
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. |
Another contribution to state the importance of this issuePlease tell me if I am missing somethingUsing 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:
|
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:
The text was updated successfully, but these errors were encountered: