Search code examples
typescriptflattenmapped-types

Flatten mapped type to intersection removing the iterated union' keys used to create it


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


Solution

  • 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; } */
    

    Playground link to code