title: How to use Typescript generics to correctly map between keys and values with indirection?
body: <p>I've been trying to wrap my head around how to write a strongly typed mapper that will map indirectly using one object's values to pick methods matching another object's keys.</p>
  <p>The actual <code>mapper</code> in the code below is not mine, it's external and javascript based. Since I cannot modify this, I have implemented a typed wrapper called <code>mapTyped</code> in the code below.</p>
  <p>This is the best attempt I've managed to do, based on other sources found on the topic.</p>
  <p>It performs well on the following goals</p>
  <ul>
  <li>ensures matching function definitions between <code>Repository</code> and <code>MappedFunctions</code></li>
  <li>prevents writing the wrong name as lookup, for example <code>mapTyped&lt;MappedFunctions&gt;()({baz: &quot;bazIntrnal&quot;})</code> will give a compiler error due to spelling</li>
  <li>works perfectly if only one item is provided in the map, the function arguments and return type will be correctly inferred</li>
  </ul>
  <p><strong>However, it does not work if I provide more than one item in the map.</strong></p>
  <p>Then all the mapped functions will have a union of all the mapped functions' definitions.</p>
  <p><strong>How do I modify <code>mapTyped</code> so that it will return unique values per &quot;row&quot; in the Record matching the key?</strong> (and not a union of values as it is now)</p>
  <p>See the comments in the minimal repro code below, <a href="https://www.typescriptlang.org/play?#code/PTAEAsBdIBwZwFwgOYEtLgK4CMB0BjAewFtg5UA7AEwCcBTOQmrOYSATxjoFoAzByMGwAbQtmDEAhpTKFMNfHWAA3ScMw9CvXFVyQ4AKA5dQANTUaA8rwA8l7ACs6+SABVOdADRmLddyboAD0g6ajhQAGs6di1QeycXfzpQAF5I6Nj45zcPAD5UuMdspIBtc3U-DwBdAG4DIw9QAFk6DEIqJIKAClxeyRpkRFBJCnYSqoBKVPyR9jrjZIAlOhhCckgmdgLlohoqGzhIGkpkbxa2jry6gxBh4WFh1VRhSRFk3kwKF1RCCkMiP6QUAfL6QH5-BCgZardabUAAMmakhgXCoADFPt9fuE0gBvAygYGEQgASQoIRoFDUkK6vGJAEEBpCKJhiNg6DQpil8gBGUAAaiJhEZpwJoGw-TJFKpwhpEpoIshh2OFGQXPyACJ5YrQBqBeL+iLPGKJQAvKUcmVyySmnXYYnCOgjdWgEoaxjEOga7wa-o0STsb26gDu6HAQbNIqqBgAvvVbnIaKApCi6FRgZiwdiGiYmsjURjQeCcaB8YS6aTyZbqaBaQymaAWWyOS6m+yaMbCWaLZSa11Iw37YRHc7prXlScAD5DkcUCbjWPxsAprhJulJv0B0BMbdFFygG5gLqSFyYNSgVDEGCOz3kyRZigX8JBaXnjbJrYwGiEBJAkbpklQHIK9hC2YMRiBd9g2OEJhlABZ02g-MOSJJN0AmAwAUOZNkKTNIuhTSEdiYfYJ1VbwyOQXJJ0o8YXTLUAsKBehICIlY1nQOE8RjOpCVQXha0Zf12FwVA4CEgMCORCYpgYwkU1wdcAFET3AAixxYkpiCqAoQSxP4tMmXjQBjUA6GEOBkjkwpf1wUIjlQBgpJgCZFKYFT8DU4gNNaLSSgABiqHS0j0h84D8nkgomYy40JFj5EfFi6jjQ8P3gjx0xXFCkNTGgDFC8EcJgJJ9gAZTM4JQiocJiL2GxSoAaW8UqmlyZqGoqkIwiAhzVQKKIYgE8rEUo5qmk6qrwnOcB2k6NJptmvIugmBAxRscaX0mqFnBI9amtARq2uaDrNu6yiCko3IujFQjmgMeixRKDrKB8CprHWmZwgG2ImiqSFSqe2pY1LUB4spWtbtmF0spoZypkkcJZhqEyl1AekAGFXAAVXpAAZVbbiY5NWhm6rIXxW5CUJQgMA5BaqBpVtWXbOpKaps0aTrYUGzbFsx15zlQEnWt+xtO0HSdOcxy6c7hZnSX5yM1KqaFTmKx1AWmebQXha6UXbUHCXR25cdeuQIXxSNuc6NZsA41uAB1ekADlXCUgARAmwCJz0LiGCmwBVmnwDpkn2kZ-nmY5W2VYNU1OYHZBIXl438hls2LZT62lbZ8tiTV+sk8bKPBZNzWY7jH2w+q1IDAY25XAACRJcqHcsRYGtKsUQF6XAUxK9bkPRTNiyuiYulxDndW7Kte2EDUYwmTtQAb5vyrRekSVxrvCV7-uMsH1Nh6LbEx4nqetRtHsZSDCtIQ1Ctr7UBel7FYPQ4uCOTYAFgAJlja4CZMCwGAUMN0ZogwP2JJA9+NB6Yah0phbEw46C4FEMgLoWQXC4AGnAAi1c4AyUAWAOQID9CQjdB6L0PpNyBh9KGDAQYjgaEQbcbAwDQBoGUAweCz4aDfhoDSL8hBVwcCmFPY8Ax-LMjoNw0u+QBYW3Tiqc2csraK1SiKVk9ltwCQWKAAA5FnAxT5Gw02GHAcgyAqRvHgoQUAMB+iSF9ihWI+iDEUFkRyAxuAui-wAMzfwAKwYSwigtBhAMG+1JnAXAZoujMLoEQgwQA" rel="nofollow noreferrer">and here's a playground that demonstrates my issue</a>.</p>
  <pre><code>// https://github.com/sindresorhus/type-fest/blob/main/source/value-of.d.ts
  type ValueOf&lt;ObjectType, ValueType extends keyof ObjectType = keyof ObjectType&gt; = ObjectType[ValueType];
  
  type MethodType = (...args: any[]) =&gt; any;
  type Repository = Record&lt;string, MethodType&gt;;
  
  // all available functions
  const functions: Repository &amp; MappedFunctions = {
    fooInternal: (fooArg: number) =&gt; 1 + fooArg,
    barInternal: (barArg: string) =&gt; &quot;barArg: &quot; + barArg,
    bazInternal: (bazArg: boolean) =&gt; [&quot;some&quot;, &quot;array&quot;, &quot;with&quot;, bazArg]
  }
  
  // our mapped functions
  type MappedFunctions = {
    fooInternal: (fooArg: number) =&gt; number,
    bazInternal: (bazArg: boolean) =&gt; (string|boolean)[]
  }
  
  // mapper for array or object 
  // (actual implementation is external to my project and I simply want to write a typed wrapper for it)
  const mapper = (map: Record&lt;string, string&gt;|string[]) =&gt; {
    const ret: Repository = {};
    if (Array.isArray(map)) {
      map.forEach(m =&gt; ret[m] = functions[m]);
    } else {
      Object.entries(map).forEach(m =&gt; ret[m[0]] = functions[m[1]]);
    }
    return ret;
  }
  
  // my typed mapper wrapper
  function mapTyped&lt;S extends Record&lt;SK, SM&gt;, SK extends string = keyof S &amp; string, SM extends MethodType = MethodType&gt;():
    &lt;M extends Record&lt;MK, SK&gt;, MK extends string = string&gt;(
    map: M
  ) =&gt; {
    [K in ValueOf&lt;M&gt; as keyof M]: S[K];
  } { return (map: any) =&gt; mapper(map) as any; }
  
  // ACTUAL:
  // const methods: {
  //     otherMethod: () =&gt; number;
  //     baz: ((fooArg: number) =&gt; number) | ((bazArg: boolean) =&gt; (string | boolean)[]);
  //     foo: ((fooArg: number) =&gt; number) | ((bazArg: boolean) =&gt; (string | boolean)[]);
  // }
  // EXPECTED:
  // const methods: {
  //     otherMethod: () =&gt; number;
  //     baz: ((bazArg: boolean) =&gt; (string | boolean)[]);
  //     foo: ((fooArg: number) =&gt; number);
  // }
  const methods =
  {
    // THIS WORKS
    //...mapTyped&lt;MappedFunctions&gt;()({baz: &quot;bazInternal&quot;}),
    // THIS FAILS
    ...mapTyped&lt;MappedFunctions&gt;()({baz: &quot;bazInternal&quot;, foo: &quot;fooInternal&quot;}),
    otherMethod: () =&gt; 42
  };
  
  // outputs: [&quot;baz&quot;, &quot;foo&quot;, &quot;otherMethod&quot;] 
  console.log(Object.keys(methods));
  
  // outputs: [&quot;some&quot;, &quot;array&quot;, &quot;with&quot;, true] 
  // but gives ts error: (property) baz: (arg0: never) =&gt; number | (string | boolean)[]
  // Argument of type 'boolean' is not assignable to parameter of type 'never'.(2345)
  console.log(methods.baz(true));
  </code></pre>
  <p>EDIT: <strong>SOLUTION</strong> thanks to the answer by @captain-yossarian this is the final code I ended up using for the typed mapper</p>
  <pre><code>const mapTyped = &lt;
    S extends Record&lt;keyof S &amp; string, MethodType&gt;,
    Keys extends keyof S &amp; string = keyof S &amp; string,
  &gt;() =&gt; {
    function anonymous&lt;Prop extends string, Mp extends Record&lt;Prop, Keys&gt;&gt;(map: Mp): {
      [P in Keys as GetKeyByValue&lt;Mp, P&gt;]: S[P]
    }
    function anonymous&lt;Prop extends string, Mp extends Record&lt;Prop, Keys&gt;&gt;(map: Mp) {
      return mapper(map)
    }
    return anonymous
  }
  </code></pre>
  <p>.. <a href="https://www.typescriptlang.org/play?#code/PTAEAsBdIBwZwFwgOYEtLgK4CMB0BjAewFtg5UA7AEwCcBTOQmrOYSATxjoFoAzByMGwAbQtmDEAhpTKFMNfHWAA3ScMw9CvXFVyQ4AKA5dQANTUaA8rwA8l7ACs6%20SABVOdADRmLddyboAD0g6ajhQAGs6di1QeycXfzpQAF5I6Nj45zcPAD5UuMdspIBtc3U-DwBdAG4DIw9QAFk6DEIqJIKAClxeyRpkRFBJCnYSqoBKVPyR9jrjZIAlOhhCckgmdgLlohoqGzhIGkpkbxa2jry6gxBh4WFh1VRhSRFk3kwKF1RCCkMiP6QUAfL6QH5-BCgZardabUAAMmakhgXCoADFPt9fuE0gBvAygYGEQgASQoIRoFDUkK6vGJAEEBpCKJhiNg6DQpil8gBGUAAaiJhEZpwJoGw-TJFKpwhpEpoIshh2OFGQXPyACJ5YrQBqBeL%20iLPGKJQAvKUcmVyySmnXYYnCOgjdWgEoaxjEOga7wa-o0STsb26gDu6HAQbNIqqBgAvvVbnIaKApCi6FRgZiwdiGiYmsjURjQeCcaB8YS6aTyZbqaBaQymaAWWyOS6m%20yaMbCWaLZSa11Iw37YRHc7prXlScAD5DkcUCbjWPxsAprhJulJv0B0BMbdFFygG5gLqSFyYNSgVDEGCOz3kyRZigX8JBaXnjbJrYwGiEBJAkbpklQHIK9hC2YMRiBd9g2OEJhlABZ02g-MOSJJN0AmAwAUOZNkKTNIuhTSEdiYfYJ1VbwyOQXJJ0o8YXTLUAsKBehICIlY1nQOE8RjOpCVQXha0Zf12FwVA4CEgMCORCYpgYwkU1wdcAFET3AAixxYkpiCqAoQSxP4tMmXjQBjUA6GEOBkjkwpf1wUIjlQBgpJgCZFKYFT8DU4gNNaLSSgABiqHS0j0h84D8nkgomYy40JFj5EfFi6jjHNkgAcVaABpaIACF2HKDQ7EcbwCrofI8TFEoAAVvxgC9HyiGIBPiKpIXiarap0l9QiocJStAAB%20UAasIOrmToZQOVjEpGsyRxo0PD94I8dMVxQpDUxoTDsSBFMknTNIbDFABlMzgh68JiL2GxZoE07EUos5WnAdoklyTtQGy9hn3OsJ0ia0B7qAhzVQKW7AYRYGVVFXIunosVQvBYYKF%20dhiDkOAbBGurur%20x7mhx37eqhZwSKx2rvC%20uBclhwiCYmSFrOq%20rPuicJJHCDLIC%20vLSpsJoYG8KrclawHqujQlYozItfmR1H0cwTHsbOkI8ZB04CZVi6Sd2fZscptmaecyEBdksU4taBKcM25yMMlsV4spOXRgVwwUqY5NnvaHEDAY3pcD2lb%20eQ9FM2LWGJi6XEzUhLUbR7GUgwrWOKwTtQNRjCYPsIDAOXOF6qBpF0ABYACZY2uBNMFgauhjdM0gw1CtG5z8A869qgNR07a-mHOhcFEZAuiyFxcEauACI7uAZMrsA5Br-RITdD0vR9TdAx9UMMCDI4NG7rC%204Hwgh89C44FwM0ul3ugZ4MIA" rel="nofollow noreferrer">and an updated playground</a></p>
  <p>BONUS: <a href="https://gist.github.com/joakimriedel/7aab32e9e69361241fc84e34d2eafab7" rel="nofollow noreferrer">This is a gist of the code that uses this typed wrapper for Vuex 4 and Vue 3</a>.</p>