title: How to create mapped types derived from other mapped types with assignable keys
body: <p>Minor Edit: This was happening for me on TS 3.0.1</p>
  
  <p>I've been running into issues using Typescript to nail down a config shape for use with a React component enhancer.  In essence, I want to specify a map-object whose properties are used by the enhancer to create injected props for the enhanced component.</p>
  
  <p>Where I've been running into issues seems to be trying to create mapped types derived from other mapped types.  In total, for the base enhancer, I have these derivations:</p>
  
  <ul>
  <li>Given a base config, whose props could be one of two shapes...
  
  <ul>
  <li>Create a normalized config whose props are the same as the props of the base config, but only of one shape.</li>
  <li>Create a component props type which has...</li>
  <li>Props that are functions, one for each base config prop.</li>
  <li>Props that are values, one for each base config prop.</li>
  </ul></li>
  </ul>
  
  <p>And for a precomposed enhancer created for a common use case:</p>
  
  <ul>
  <li>A <code>mapDispatchToProps</code> function which had as its return type props derived from a slightly different dispatch-aware version of config.</li>
  <li>A derivation of a base config from that dispatch-aware config.</li>
  </ul>
  
  <p>The issues I tend to run into here are:</p>
  
  <ul>
  <li>Non-assignability of keys: <code>Type 'Extract&lt;keyof TBase, string&gt;' is not assignable to type 'Extract&lt;keyof TRel, string&gt;'.</code></li>
  <li>Non-indexability of base type when derived from related type: <code>Type 'Extract&lt;keyof TRel, string&gt;' cannot be used to index type 'TBase'.</code>
  
  <ul>
  <li>Basically the same as the first one but at the point of assignment.</li>
  </ul></li>
  <li>TypeScript language server request or compiler dying with <code>RangeError: Maximum call stack size exceeded</code>, probably due to lots of conditional types and mapped types.</li>
  </ul>
  
  <p>My questions then are:</p>
  
  <ul>
  <li>Is this possible to accomplish in TypeScript?</li>
  <li>If so, there a better way to do it than what I did while both sticking to plain objects and still retaining the per-prop type constraints?</li>
  <li>Why are two sets of keys-unions derived from two mapped types non-assignable when they're both ultimately derived from only one mapped type?  Is it because even though they are both derived from one, TS only evaluates the keys-unions assignability by the parameter constraints rather than the specific instantiated types?  Or something else I'm missing?</li>
  </ul>
  
  <p>Here is a small example that seems to elicit the primary errors I'm running into:</p>
  
  <p>EDIT: <a href="https://www.typescriptlang.org/play/index.html#src=%2F%2F%20NOTE%3A%20Crashes%20the%20playground%20in%20chrome%20on%20my%20computer%3A%0D%0A%2F%2F%20%20%20RangeError%3A%20Maximum%20call%20stack%20size%20exceeded%0D%0A%2F%2F%20Also%20probably%20crashes%20a%20tsserver%20process%2Frequest%2Fthing%20because%0D%0A%2F%2F%20vscode%20stops%20updating%20the%20error%20squigglies%20after%20a%20bit.%0D%0A%0D%0A%2F%2F%20Convenience.%0D%0Atype%20PropKeyOf%3CT%3E%20%3D%20Extract%3Ckeyof%20T%2C%20string%3E%3B%0D%0A%0D%0A%2F%2F%20A%20%22related%22%20type%20that%20I%20want%20to%20be%20related%20to%20a%20%22base%22%20type.%0D%0A%2F%2F%20Particularly%2C%20I%20want%20to%20be%20able%20to%20derive%20the%20%22base%22%20type.%0D%0A%2F%2F%20%22related%22%20and%20%22base%22%20are%20used%20here%20because%20these%20are%20config%20types%0D%0A%2F%2F%20for%20the%20interface%20of%20a%20related%20enhancer%20utility%20and%0D%0A%2F%2F%20a%20base%20enhancer%20utility%20respectively.%0D%0A%2F%2F%20They%20are%20otherwise%20unrelated.%0D%0A%0D%0Atype%20RelatedMap%3CT%3E%20%3D%20%7B%0D%0A%20%20%5BK%20in%20PropKeyOf%3CT%3E%5D%3A%20RelatedMapPropType%3CT%5BK%5D%3E%3B%0D%0A%7D%3B%0D%0A%0D%0Atype%20RelatedMapPropType%3CT%3E%20%3D%20T%20extends%20RelatedMapProp%3Cinfer%20V%3E%20%3F%20RelatedMapProp%3CV%3E%20%3A%20never%3B%0D%0A%0D%0Atype%20RelatedMapProp%3CV%3E%20%3D%20%7B%20foo%3A%20V%2C%20init%28%29%3A%20V%20%7D%3B%0D%0A%0D%0A%2F%2F%20A%20%22base%22%20type%20that%20I%20want%20to%20use%20for%20a%20%22base%22%20interface.%0D%0A%0D%0Atype%20BaseMap%3CT%3E%20%3D%20%7B%0D%0A%20%20%5BK%20in%20PropKeyOf%3CT%3E%5D%3A%20BaseMapPropType%3CT%5BK%5D%3E%3B%0D%0A%7D%3B%0D%0A%0D%0Atype%20BaseMapPropType%3CT%3E%20%3D%20T%20extends%20BaseMapProp%3Cinfer%20V%3E%20%3F%20BaseMapProp%3CV%3E%20%3A%20never%3B%0D%0A%0D%0Atype%20BaseMapProp%3CV%3E%20%3D%20%7B%20baz%3A%20V%2C%20init%28%29%3A%20V%20%7D%3B%0D%0A%0D%0A%2F%2F%20Make%20the%20conversion%20type%0D%0Atype%20BaseMapOfRelatedMap%3CTRel%20extends%20RelatedMap%3CTRel%3E%3E%20%3D%20%7B%0D%0A%20%20%5BK%20in%20PropKeyOf%3CTRel%3E%5D%3A%20BasePropOfRelatedMapProp%3CTRel%5BK%5D%3E%3B%0D%0A%7D%0D%0A%0D%0Atype%20BasePropOfRelatedMapProp%3CTRelProp%3E%20%3D%20TRelProp%20extends%20RelatedMapProp%3Cinfer%20V%3E%20%3F%20BaseMapProp%3CV%3E%20%3A%20never%3B%0D%0A%0D%0Afunction%20isOwnProp%3CO%20extends%20%7B%7D%3E%28o%3A%20O%2C%20pn%3A%20string%29%3A%20pn%20is%20PropKeyOf%3CO%3E%20%7B%0D%0A%20%20return%20%21%21o%20%26%26%20%28typeof%20o%20%3D%3D%3D%20&#39;object&#39;%29%20%26%26%20Object.prototype.hasOwnProperty.call%28o%2C%20pn%29%3B%0D%0A%7D%0D%0A%0D%0Afunction%20createBaseMapOfRelatedMap%3C%0D%0A%20%20TRel%20extends%20RelatedMap%3CTRel%3E%2C%0D%0A%20%20%2F%2F%20Error%3A%0D%0A%20%20%2F%2F%20-%20%5Bts%5D%20Type%20&#39;BaseMapOfRelatedMap%3CTRel%3E&#39;%20does%20not%20satisfy%20the%20constraint%20&#39;BaseMap%3CTBase%3E&#39;.%0D%0A%20%20%2F%2F%20%20%20-%20Type%20&#39;Extract%3Ckeyof%20TBase%2C%20string%3E&#39;%20is%20not%20assignable%20to%0D%0A%20%20%2F%2F%20%20%20%20%20type%20&#39;Extract%3Ckeyof%20TRel%2C%20string%3E&#39;.%0D%0A%20%20TBase%20extends%20BaseMap%3CTBase%3E%20%3D%20BaseMapOfRelatedMap%3CTRel%3E%0D%0A%3E%28foo%3A%20TRel%29%3A%20TBase%20%7B%0D%0A%20%20const%20baz%20%3D%20%7B%7D%20as%20TBase%3B%0D%0A%0D%0A%20%20for%20%28const%20propName%20in%20foo%29%20if%20%28isOwnProp%28foo%2C%20propName%29%29%20%7B%0D%0A%20%20%20%20%2F%2F%20Errors%3A%0D%0A%20%20%20%20%2F%2F%20-%20%5Bts%5D%20Type%20&#39;Extract%3Ckeyof%20TRel%2C%20string%3E&#39;%20cannot%20be%20used%0D%0A%20%20%20%20%2F%2F%20%20%20to%20index%20type%20&#39;TBase&#39;.%0D%0A%20%20%20%20%2F%2F%20-%20%5Bts%5D%20Property%20&#39;foo&#39;%20does%20not%20exist%0D%0A%20%20%20%20%2F%2F%20%20%20on%20type%20&#39;TRel%5BExtract%3Ckeyof%20TRel%2C%20string%3E%5D&#39;.%0D%0A%20%20%20%20baz%5BpropName%5D%20%3D%20%7B%20baz%3A%20foo%5BpropName%5D.foo%2C%20init%3A%20foo%5BpropName%5D.init%20%7D%3B%0D%0A%20%20%7D%0D%0A%0D%0A%20%20return%20baz%3B%0D%0A%7D%0D%0A" rel="nofollow noreferrer">Link to the TS playground with this in it</a> (it just crashes the compiler on my computer/browser, though)</p>
  
  <pre><code>// NOTE: Crashes the playground in chrome on my computer:
  //   RangeError: Maximum call stack size exceeded
  // Also probably crashes a tsserver process/request/thing because
  // vscode stops updating the error squigglies after a bit.
  
  // Convenience.
  type PropKeyOf&lt;T&gt; = Extract&lt;keyof T, string&gt;;
  
  // A "related" type that I want to be related to a "base" type.
  // Particularly, I want to be able to derive the "base" type.
  // "related" and "base" are used here because these are config types
  // for the interface of a related enhancer utility and
  // a base enhancer utility respectively.
  // They are otherwise unrelated.
  
  type RelatedMap&lt;T&gt; = {
    [K in PropKeyOf&lt;T&gt;]: RelatedMapPropType&lt;T[K]&gt;;
  };
  
  type RelatedMapPropType&lt;T&gt; = T extends RelatedMapProp&lt;infer V&gt; ? RelatedMapProp&lt;V&gt; : never;
  
  type RelatedMapProp&lt;V&gt; = { foo: V, init(): V };
  
  // A "base" type that I want to use for a "base" interface.
  
  type BaseMap&lt;T&gt; = {
    [K in PropKeyOf&lt;T&gt;]: BaseMapPropType&lt;T[K]&gt;;
  };
  
  type BaseMapPropType&lt;T&gt; = T extends BaseMapProp&lt;infer V&gt; ? BaseMapProp&lt;V&gt; : never;
  
  type BaseMapProp&lt;V&gt; = { baz: V, init(): V };
  
  // Make the conversion type
  type BaseMapOfRelatedMap&lt;TRel extends RelatedMap&lt;TRel&gt;&gt; = {
    [K in PropKeyOf&lt;TRel&gt;]: BasePropOfRelatedMapProp&lt;TRel[K]&gt;;
  }
  
  type BasePropOfRelatedMapProp&lt;TRelProp&gt; = TRelProp extends RelatedMapProp&lt;infer V&gt; ? BaseMapProp&lt;V&gt; : never;
  
  function isOwnProp&lt;O extends {}&gt;(o: O, pn: string): pn is PropKeyOf&lt;O&gt; {
    return !!o &amp;&amp; (typeof o === 'object') &amp;&amp; Object.prototype.hasOwnProperty.call(o, pn);
  }
  
  function createBaseMapOfRelatedMap&lt;
    TRel extends RelatedMap&lt;TRel&gt;,
    // Error:
    // - [ts] Type 'BaseMapOfRelatedMap&lt;TRel&gt;' does not satisfy the constraint 'BaseMap&lt;TBase&gt;'.
    //   - Type 'Extract&lt;keyof TBase, string&gt;' is not assignable to
    //     type 'Extract&lt;keyof TRel, string&gt;'.
    TBase extends BaseMap&lt;TBase&gt; = BaseMapOfRelatedMap&lt;TRel&gt;
  &gt;(foo: TRel): TBase {
    const baz = {} as TBase;
  
    for (const propName in foo) if (isOwnProp(foo, propName)) {
      // Errors:
      // - [ts] Type 'Extract&lt;keyof TRel, string&gt;' cannot be used
      //   to index type 'TBase'.
      // - [ts] Property 'foo' does not exist
      //   on type 'TRel[Extract&lt;keyof TRel, string&gt;]'.
      baz[propName] = { baz: foo[propName].foo, init: foo[propName].init };
    }
  
    return baz;
  }
  </code></pre>
  
  <h1>Edit 1</h1>
  
  <p>Thanks for the help, Matt!</p>
  
  <p>NOTE: fixed the example names up.</p>
  
  <h2>On <code>TBase</code></h2>
  
  <blockquote>
    <p>As for the specific error that <code>'Extract&lt;keyof TRel, string&gt;' cannot be used to index type 'TBase'</code>, this is because <code>TRel</code> and <code>TBase</code> are independent type parameters; <code>TBase</code> has a default, but it can be overridden by a caller. So there's nothing to prevent <code>TRel</code> from having properties that <code>TBase</code> does not.</p>
  </blockquote>
  
  <p>That makes sense, good point, I wasn't really thinking of that at the time, kinda had my head buried deep in one way of thinking.  Guess that means I can't use type params to shorten that unless I want to add more <code>extends ...</code> constraints.</p>
  
  <p>So, like this:</p>
  
  <pre><code>// added to try to typecheck created prop.
  function createBasePropOfRelatedMapProp&lt;
    TRelProp extends RelatedMapProp&lt;TRelProp&gt;,
  &gt;(fooProp: TRelProp): BasePropOfRelatedMapProp&lt;TRelProp&gt; {
    return { baz: fooProp.foo, init: fooProp.init };
  }
  
  function createBaseMapOfRelatedMap&lt;
    TRel extends RelatedMap&lt;TRel&gt;,
  &gt;(foo: TRel): BaseMapOfRelatedMap&lt;TRel&gt; {
    const baz = {} as BaseMapOfRelatedMap&lt;TRel&gt;;
  
    for (const propName in foo) if (isOwnProp(foo, propName)) {
      baz[propName] = createBasePropOfRelatedMapProp(foo[propName]);
    }
  
    return baz;
  }
  
  function logBaseMap&lt;TBase extends BaseMap&lt;TBase&gt;&gt;(base: TBase): void {
    for (const propName in base) if (isOwnProp(base, propName)) {
      console.log(propName, '=&gt;', base[propName]);
    }
  }
  </code></pre>
  
  <p>Unfortunately, this is crashing the tsserver request again:</p>
  
  <pre><code>Err 551   [15:35:42.708] Exception on executing command delayed processing of request 12:
  
      Maximum call stack size exceeded
  
      RangeError: Maximum call stack size exceeded
      at getSimplifiedIndexedAccessType (/.../client/node_modules/typescript/lib/tsserver.js:37544:48)
      at getSimplifiedType (/.../client/node_modules/typescript/lib/tsserver.js:37540:63)
      at getConstraintOfDistributiveConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35523:54)
      at getConstraintOfConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35535:20)
      at getConstraintOfType (/.../client/node_modules/typescript/lib/tsserver.js:35496:62)
      at getConstraintOfDistributiveConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35523:34)
      at getConstraintOfConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35535:20)
      at getConstraintOfType (/.../client/node_modules/typescript/lib/tsserver.js:35496:62)
      (... repeat ad nauseum)
  </code></pre>
  
  <p>Alas.</p>
  
  <h2>Original Context</h2>
  
  <p>I tried to simplify the example to the bare minimum to illustrate the errors, but this of course lost the original context, even if I stated the context in the description of the problem.</p>
  
  <p>The original code essentially works something like this:</p>
  
  <pre><code>const config = {
    // sometimes I only need just the request itself.
    foo: (ownProps: ComponentOwnProps) =&gt; () =&gt; apiFetch(`/api/foos/${ownProps.fooId}`),
  
    // sometimes I need more control.
    bar: {
      request: (ownProps: ComponentOwnProps) =&gt; (barId: string) =&gt; apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
      reduce: (
        prevPropValue: { [k: string]: AsyncData&lt;APIResponse&gt; },
        nextResValue: AsyncData&lt;APIResponse&gt;,
        ownProps: ComponentOwnProps,
        [barId]: [string]
      ) =&gt; ({
        ...prevPropValue,
        [barId]: nextResValue,
      }),
      initial: () =&gt; ({} as { [k: string]: AsyncData&lt;APIResponse&gt; })
    },
  };
  
  const enhanceComponent = withAsyncData(config);
  </code></pre>
  
  <p>I wanted to then use the mapped type constraints to ensure that all props on the <code>config</code> shared the same <code>OwnProps</code> type and that each prop itself was internally consistent with regards to the types used therein, mostly noticeable in <code>bar</code>, where for instance <code>reduce</code> should return the same type as its <code>prevPropValue</code> argument, and that <code>initial</code> should also return that same type; but also that the last array argument to <code>reduce</code> is a tuple of the args types of the function returned by <code>request</code>.</p>
  
  <p>As part of this, I needed to then generate a type for the props that get injected by this config:</p>
  
  <ul>
  <li><code>props.getAsyncData.foo(): Promise&lt;AsyncData&lt;APIResponse&gt;&gt;</code></li>
  <li><code>props.getAsyncData.bar(barId: string): Promise&lt;AsyncData&lt;APIResponse&gt;&gt;</code></li>
  <li><code>props.asyncData.foo: AsyncData&lt;APIResponse&gt;</code></li>
  <li><code>props.asyncData.bar: AsyncData&lt;APIResponse&gt;</code></li>
  </ul>
  
  <p>I then wanted a variation on the above config for use with a precomposition of <code>withAsyncData</code> with React-Redux's <code>connect</code>, which ended up looking like this:</p>
  
  <pre><code>const config = {
    foo: (dispatch: AppDispatch) =&gt; (ownProps: ComponentOwnProps) =&gt; () =&gt; apiFetch(`/api/foos/${ownProps.fooId}`),
    bar: {
      request: (dispatch: AppDispatch) =&gt; (ownProps: ComponentOwnProps) =&gt; (barId: string) =&gt; apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
      reduce: (
        prevPropValue: { [k: string]: AsyncData&lt;APIResponse&gt; },
        nextResValue: AsyncData&lt;APIResponse&gt;,
        ownProps: ComponentOwnProps,
        [barId]: [string]
      ) =&gt; ({
        ...prevPropValue,
        [barId]: nextResValue,
      }),
      initial: () =&gt; ({} as { [k: string]: AsyncData&lt;APIResponse&gt; })
    },
  };
  
  const enhanceComponent = withConnectedAsyncData(config);
  </code></pre>
  
  <p>The precomposition is (essentially) just <code>config =&gt; compose(connect(null, createMapDispatchToProps(config)), withAsyncData(createAsyncDataConfig(config)))</code>.  But of course I need to create a base config type derived from that (slightly) extended config type using <code>createAsyncDataConfig()</code>.</p>