Seeking to achieve Flatten<T>
as the intersection of all properties in T
, one can utilize a method akin to UnionToIntersection
illustrated in Transform union type to intersection type. By placing types to intersect in a contravariant position and leveraging infer
, we can infer a single type:
type Flatten<T extends object> = { [K in keyof T]: (x: T[K]) => void } extends
Record<keyof T, (x: infer I) => void> ? I : never;
This transforms, for example,
{a: {x: string}, b: {y: number}, c: {z: boolean}}
into
{x: string} & {y: number} & {z: boolean}
. To avoid a large intersection, consider combining it using a domain-like
mapped 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;
The result would be
{x: string; y: number; z: boolean}
.
Let's put it to the test:
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";
};
} */
Validation successful.
Note that you could use UnionToIntersection
directly like this:
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]>
To achieve practically the same result:
type DesiredResult = Flatten<Result>;
/* type DesiredResult = Record<"a_1", { v: "a_2"; }> & Record<"b_1", { v: "b_2"; }> */
However, if any of the type's properties are unions themselves, they'll also end up being intersected. For instance, with
type X = Flatten<{ a: { x: string } | { y: number }, b: { z: boolean } }>;
The expected output would look like
/* type X = { x: string; z: boolean; } | { y: number; z: boolean; } */
But using UnionToIntersection
would lead to
/* type X = { x: string; } & { y: number; } & { z: boolean; } */
Playground link to code