Search code examples
typescriptmapped-types

How to merge a map of types into a single flat type in TypeScript


What I need

I have an undetermined number of mods in an input object:

const mod1 = {
  actions: {
    a() { },
    b() { },
  }
}

const mod2 = {
  actions: {
    c() { },
    d() { },
  }
}

const input = {
  mods: {
    mod1,
    mod2
  }
}

At run time, a lib merges the mods in a single object which is equivalent to:

const output = {
  actions: {
    a() { },
    b() { },
    c() { },
    d() { },
  }
}

And I would like to create a type that would describe this single object.

What I tried

The input objects can be described like that:

interface Input {
  mods: Mods
}

interface Mods {
  [name: string]: Mod
}

interface Mod {
  actions: {
    [name: string]: () => void
  }
}

Then, I don't know how to merge the content of mods:

interface ToOutput<I extends Input> {
  actions: MergeMods<I["mods"]>
}

type MergeMods<M extends Mods> = // How to merge the content of 'M'?

Solution

  • Here is a solution:

    type Output = ToOutput<(typeof input)["mods"]>
    
    interface ToOutput<I extends Mods> {
      actions: UnionToIntersection<I[keyof I]["actions"]>
    }
    
    type UnionToIntersection<U> =
      (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
    

    Explanations

    The following type:

    interface ToOutput<I extends Input> {
        actions: UnionToIntersection<I["mods"][keyof I["mods"]]["actions"]>
    }
    

    first uses keyof and lookups to map the Input type to match the output structure

    interface ToOutputStep1<I extends Input> {
        actions: I["mods"][keyof I["mods"]]["actions"]
    }
    
    /*
    type T1 = {
        actions:
          | { a: {}; b: {}; }
          | { c: {}; d: {}; };
    }
    */
    type T1 = ToOutputStep1<typeof input>
    

    and then converts the actions union type to an intersection type.

    /*
    type T2 = {
        actions: {
            a: {};
            b: {};
            c: {};
            d: {};
        };
    }
    */
    
    type T2 = ToOutput<typeof input>
    

    Playground