title: Keep specific function signatures in a generic type constraint - TypeScript
body: <p>At first: sorry for the long post and that in fact I have two different questions here. But since both are so closely related to each other (two different approaches to reach the same goals) I thought it's better to keep them in one post.</p>
  <p>I am trying to implement a lightweight TypeScript signalling / event framework, where</p>
  <ul>
  <li>all possible signal names are known and have IntelliSense support</li>
  <li>their signatures (arguments and return value) are strongly typed and have IntelliSense support as well</li>
  </ul>
  <p>I know there exist some solutions which achieve similar goals somehow, but I have a specific
  simple to use design in mind which makes use of the newer TypeScript generic index type constraint features - but for now I am obviously not capable to master them on my own :)</p>
  <p>I am using TypeScript 4.2.3.</p>
  <p>I tried two different approaches with different non-working outcomes...</p>
  <p><strong>First approach</strong></p>
  <p>This compiles but we don't have any types on the signal arguments and return values:</p>
  <pre class="lang-js prettyprint-override"><code>// Define signals by name and a function, which specifies SignalArgs
  // and SignalValue (return value).
  type Signals = {
    hello: (name: string) =&gt; string;
    add: (val1: number, val2: number) =&gt; number;
  };
  
  // Generic class which should implement on() and emit() with arguments
  // bound to the specific signals enumerated in the Signals type.
  class SignalBus&lt;Signals&gt; {
    on&lt;Key extends string &amp; keyof Signals, Spec extends (...args: any) =&gt; any &amp; Signals[Key]&gt;(
      signal: Key,
      handler: (
        signal: Signal&lt;Parameters&lt;Spec&gt;, ReturnType&lt;Spec&gt;&gt;,
        ...args: Parameters&lt;Spec&gt;
      ) =&gt; Promise&lt;ReturnType&lt;Spec&gt;&gt;,
    ): SignalConnection&lt;Parameters&lt;Spec&gt;, ReturnType&lt;Spec&gt;&gt; {
      // ...
      console.log(&quot;on&quot;, signal, handler);
      const connection = (handler as unknown) as SignalConnection&lt;Parameters&lt;Spec&gt;, ReturnType&lt;Spec&gt;&gt;;
      connection.signal = signal;
      return connection;
    }
  
    async emit&lt;Key extends string &amp; keyof Signals, Spec extends (...args: any) =&gt; any &amp; Signals[Key]&gt;(
      signal: Key,
      ...args: Parameters&lt;Spec&gt;
    ): Promise&lt;Signal&lt;Parameters&lt;Spec&gt;, ReturnType&lt;Spec&gt;&gt;&gt; {
      // ...
      console.log(&quot;emit&quot;, signal, args);
      return new Signal&lt;Parameters&lt;Spec&gt;, ReturnType&lt;Spec&gt;&gt;(signal, args);
    }
  }
  
  // Signal objects represent a signal at runtime
  class Signal&lt;SignalArgs extends unknown[] = [], SignalValue = any&gt; {
    public value: SignalValue;
    constructor(public signal: string, public args: SignalArgs) {}
  }
  
  // SignalConnection objects enclose the handler callback, signal name and probably more
  type SignalConnection&lt;SignalArgs extends unknown[], SignalResult&gt; = {
    (signal: Signal&lt;SignalArgs, SignalResult&gt;, ...args: SignalArgs): Promise&lt;SignalResult | void&gt;;
    signal: string;
  };
  
  // Create a SignalBus specific to the Signals defined above
  const bus = new SignalBus&lt;Signals&gt;();
  
  // Fine
  bus.on(&quot;hello&quot;, async (_signal, name) =&gt; `Hello ${name}`);
  
  // Should give type error, because two numeric arguments and numeric return value are expected
  bus.on(&quot;add&quot;, async (_signal, name: string) =&gt; `Shouldn't be accepted ${name}`);
  
  // Should give type error, because only one string argument is expected
  bus.emit(&quot;hello&quot;, &quot;World&quot;, &quot;shouldn't be accepted&quot;);
  </code></pre>
  <p>The problem is that the specific function signature, e.g. in the on() Method, gets lost:</p>
  <pre class="lang-js prettyprint-override"><code>on&lt;Key extends string &amp; keyof Signals, Spec extends (...args: any) =&gt; any &amp; Signals[Key]&gt;(...)
  </code></pre>
  <p>This happens with the use of <code>Spec extends (...args: any) =&gt; any</code>.</p>
  <p>I understand that I have to constraint the <code>Signals[Key]</code> property to be a callable
  function to make the <code>Parameters&lt;&gt;</code> and <code>ReturnValue&lt;&gt;</code> utility types work.</p>
  <p>But how can I do that without loosing the type information of the <code>Signals[Key]</code> functions?</p>
  <p>Is there any way to achieve what I am trying to do here?</p>
  <p><strong>Another approach, another problem ;)</strong></p>
  <p>Here I definine a generic <code>signal&lt;&gt;</code> type which consists of the signal parameter and return value types and accessing them via index - but with no luck as well...</p>
  <p>When using the SignalBus class the compiler complains about non assignable <code>never</code> types. See the comments at the bottom of the code example:</p>
  <pre class="lang-js prettyprint-override"><code>// Generic type to specify signal arguments and return value.
  type signal&lt;SignalArgs extends unknown[] = [], SignalResult = void&gt; = {
    args: SignalArgs;
    result: SignalResult;
  };
  
  // Define signals by name and the generic signal&lt;&gt; type.
  type Signals = {
    hello: signal&lt;[name: string], string&gt;;
    add: signal&lt;[val: number, val2: number], string&gt;;
  };
  
  // Generic class which should implement on() and emit() with arguments
  // bound to the specific signals enumerated in the Signals type.
  class SignalBus&lt;Signals&gt; {
    on&lt;Key extends string &amp; keyof Signals, Spec extends signal &amp; Signals[Key]&gt;(
      signal: Key,
      handler: (
        signal: Signal&lt;Spec[&quot;args&quot;], Spec[&quot;result&quot;]&gt;,
        ...args: Spec[&quot;args&quot;]
      ) =&gt; Promise&lt;Spec[&quot;result&quot;]&gt;,
    ): SignalConnection&lt;Spec[&quot;args&quot;], Spec[&quot;result&quot;]&gt; {
      // ...
    }
  
    async emit&lt;Key extends string &amp; keyof Signals, Spec extends signal &amp; Signals[Key]&gt;(
      signal: Key,
      ...args: Spec[&quot;args&quot;]
    ): Promise&lt;Signal&lt;Spec[&quot;args&quot;], Spec[&quot;result&quot;]&gt;&gt; {
      // ...
    }
  }
  
  // Signal objects represent a signal at runtime
  class Signal&lt;SignalArgs extends unknown[] = [], SignalValue = any&gt; {
    public value: SignalValue;
    constructor(public signal: string, public args: SignalArgs) {}
  }
  
  // SignalConnection objects enclose the handler callback, signal name and probably more
  type SignalConnection&lt;SignalArgs extends unknown[], SignalResult&gt; = {
    (signal: Signal&lt;SignalArgs, SignalResult&gt;, ...args: SignalArgs): Promise&lt;SignalResult | void&gt;;
    signal: string;
  };
  
  // Create a SignalBus specific to the Signals defined above
  const bus = new SignalBus&lt;Signals&gt;();
  
  // Type 'Promise&lt;string&gt;' is not assignable to type 'Promise&lt;never&gt;'.
  //   Type 'string' is not assignable to type 'never'. ts(2322)
  bus.on(&quot;hello&quot;, async (_signal, name) =&gt; `Hello ${name}`);
  
  // Argument of type '(_signal: Signal&lt;never, never&gt;, name: string) =&gt; Promise&lt;string&gt;' is not assignable to parameter of type '(signal: Signal&lt;never, never&gt;, ...args: never) =&gt;
  // Promise&lt;never&gt;'.
  //   Type 'Promise&lt;string&gt;' is not assignable to type 'Promise&lt;never&gt;'. ts(2345)
  bus.on(&quot;add&quot;, async (_signal, name: string) =&gt; `Shouldn't be accepted ${name}`);
  
  // Argument of type 'string' is not assignable to parameter of type 'never'. ts(2345)
  bus.emit(&quot;hello&quot;, &quot;World&quot;, &quot;shouldn't be accepted&quot;);
  </code></pre>
  <p>It looks the type constraint <code>Spec extends signal &amp; Signals[Key]</code> does never
  match. Why?</p>
  <p>Does anyone have any idea on this?</p>