I have a union of types having a property also containing union types:
type Union =
| {
name: "a";
event:
| { eventName: "a1"; payload: string }
| { eventName: "a2"; payload: number };
}
| {
name: "b";
event: { eventName: "b1"; payload: boolean };
};
I'm trying to flatten it to get a single union type like this:
type Result =
| { name: "a"; eventName: "a1"; payload: string }
| { name: "a"; eventName: "a2"; payload: number }
| { name: "b"; eventName: "b1"; payload: boolean };
I tried to use mapped typed and various utility types but couldn't figure it out. Is it something that can actually be done?
The utility type converting a union to an intersection was fundamental: I've taken it from here
type Union =
| {
name: "a";
event:
| { eventName: "a1"; payload: string }
| { eventName: "a2"; payload: number };
}
| {
name: "b";
event: { eventName: "b1"; payload: boolean };
};
type nested = {
[n in Union['name']]: {
[e in Extract<Union, { name: n }>['event']['eventName']]: {
name: n,
eventName: e,
payload: Extract<Union['event'], {eventName: e}>['payload']
}
}
}
// https://stackoverflow.com/a/50375286/3370341
type UnionToIntersection<U> =
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type r = UnionToIntersection<nested[keyof nested]>
type Result = r[keyof r]
type Expected =
| { name: "a"; eventName: "a1"; payload: string }
| { name: "a"; eventName: "a2"; payload: number }
| { name: "b"; eventName: "b1"; payload: boolean };
declare const expected: Expected;
declare const result: Result;
// In case types are not the same I expect a type error here
const y: Result = expected
const l: Expected = result
I wonder if there is an easier way.
I've started by creating a nested object type, but with the aim of putting name
and eventName
on the same level.
The intermediate result was something like this:
type nested = {
a: {
a1: {
name: 'a',
eventName: 'a1',
payload: string
},
a2: {
name: 'a',
eventName: 'a2',
payload: number
}
},
b: {
b1: {
name: 'b',
eventName: 'b1',
payload: boolean
}
}
}
Then I've extracted a union of only the inner values types.
I've tried using things like Extract
on eventName
, but while it works for 'b1'
, it leads to never
with 'a1'
and 'a2'
.
type b1 = Extract<Union, {event: {eventName: 'b1'}}> // ok
type a1 = Extract<Union, {event: {eventName: 'a1'}}> // never
Much uglier solution than @Ilario Pierbattista, but works:
//https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
//https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
// https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union#comment-94748994
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type ArrayOfUnions<T> = IsUnion<T> extends true ? UnionToArray<T> : T
//https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
type Distributive<T> = [T] extends [any] ? T : never
// treat this predicate as Array.prototype.map predicate
type MapPredicate<T> =
T extends { event: object, name: string }
? IsUnion<T['event']> extends true
? ArrayOfUnions<T['event']> extends any[]
? Distributive<ArrayOfUnions<T['event']>[number]> & { name: T['name'] }
: T
: T['event'] & { name: T['name'] }
: never
//https://catchts.com/tuples#map
// This util works similar to Array.prototype.map
type Mapped<
Arr extends Array<unknown>,
Result extends Array<unknown> = []
> = Arr extends []
? []
: Arr extends [infer H]
? [...Result, MapPredicate<H>]
: Arr extends [infer Head, ...infer Tail]
? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
: Readonly<Result>;
type Union =
| {
name: "a";
event:
| { eventName: "a1"; payload: string }
| { eventName: "a2"; payload: number };
}
| {
name: "b";
event: { eventName: "b1"; payload: boolean };
};
type Result = Mapped<UnionToArray<Union>>[number]
type Expected =
| { name: "a"; eventName: "a1"; payload: string }
| { name: "a"; eventName: "a2"; payload: number }
| { name: "b"; eventName: "b1"; payload: boolean };
type Assert = Result extends Expected ? true : false // true
Algorithm:
Here you can find more examples
I have a union of types having a property also containing union types:
type Union =
| {
name: "a";
event:
| { eventName: "a1"; payload: string }
| { eventName: "a2"; payload: number };
}
| {
name: "b";
event: { eventName: "b1"; payload: boolean };
};
I'm trying to flatten it to get a single union type like this:
type Result =
| { name: "a"; eventName: "a1"; payload: string }
| { name: "a"; eventName: "a2"; payload: number }
| { name: "b"; eventName: "b1"; payload: boolean };
I tried to use mapped typed and various utility types but couldn't figure it out. Is it something that can actually be done?
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.