I am not sure how to better phrase the question but I hope the question title with the code below is self-explanatory, despite it might seem useless.
EDIT reduced code to the bare minimum:
type MyUnion = "a" | "b";
type Result = {
[K in MyUnion]: Record<`${K}_1`, { v: `${K}_2` }>;
};
// type Result = { a: Record<"a_1", { v: "a_2"; }>; b: Record<"b_1", { v: "b_2"; }>; };
type DesiredResult = { "a_1": { v: "a_2" }; "b_1": { v: "b_2" }; };
I don't understand how to get DesiredResult
from Result
You're looking for Flatten<T>
to effectively become the intersection of all the properties of T
. We can do that by using a technique much like UnionToIntersection
as shown in Transform union type to intersection type, where we put the types to intersect in a contravariant position (see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript) and infer
a single type for it:
type Flatten<T extends object> = { [K in keyof T]: (x: T[K]) => void } extends
Record<keyof T, (x: infer I) => void> ? I : never;
That turns, say, {a: {x: string}, b: {y: number}, c: {z: boolean}}
into {x: string} & {y: number} & {z: boolean}
. If you don't want to see a big intersection, you could use an identity-like mapped type to combine the intersection into a single object type:
type Flatten<T extends object> = { [K in keyof T]: (x: T[K]) => void } extends
Record<keyof T, (x: infer I) => void> ? { [K in keyof I]: I[K] } : never;
Now you'd get {x: string; y: number; z: boolean}
.
Let's test it:
type Result = {
a: Record<"a_1", {
v: "a_2";
}>;
b: Record<"b_1", {
v: "b_2";
}>;
}
type DesiredResult = Flatten<Result>;
/* type DesiredResult = {
a_1: {
v: "a_2";
};
b_1: {
v: "b_2";
};
} */
Looks good.
Note that you could use UnionToIntersection
directly and write
type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends
((x: infer I) => void) ? I : never
type Flatten<T extends object> = UnionToIntersection<T[keyof T]>
and get effectively the same type
type DesiredResult = Flatten<Result>;
/* type DesiredResult = Record<"a_1", { v: "a_2"; }> & Record<"b_1", { v: "b_2"; }> */
but if any of your type's properties are themselves unions you'd end up intersecting them too, which I don't think you want. That is, the desired output of
type X = Flatten<{ a: { x: string } | { y: number }, b: { z: boolean } }>;
looks like
/* type X = { x: string; z: boolean; } | { y: number; z: boolean; } */
but using UnionToIntersection
gives you
/* type X = { x: string; } & { y: number; } & { z: boolean; } */