Search code examples
typescripttypescript-typingstypescript-class

Have multiple function signatures in a function-typed member variable


Why is it not possible to have multiple signatures for a "handler" function variable?

Take this ideal, but invalid code snippet as an example:

class MyEntityService {

    private handleThing: (a: undefined) => undefined;
    private handleThing: <T extends object>(a: T) => T;
    private handleThing: <T extends object>(a: T | undefined) => object | T {
        if (!a) return undefined;
        // Various calls to other private methods here
        return a;
    }
}

What I actually want is something along those lines: handleThing is an event handler or a promise's then body that needs to have lexical this binding (an arrow function is the easiest way to achieve that). The intention is that handleThing maintains multiple signatures, so that the most suitable one may be picked up by context (i.e. depending on where it is used).

I also tried the folllowing, but the handler variable ended up having type any, i.e. all the typing was effectively dropped off:

class MyEntityService {

    private handleThing = this.handleThing_.bind(this);
    private handleThing_(a: undefined): undefined;
    private handleThing_<T extends object>(a: T): T;
    private handleThing_<T extends object>(a: T | undefined): T | undefined {
        if (!a) return undefined;
        // Various calls to other private methods here
        return a;            
    }
}

Having only handleThing: <T extends object>(a: T | undefined) => object | undefined is not ideal:

  1. It doesn't map to the actual function's signature as much as I need, i.e. when my input is not null-like, my return type is assuredly not-null as well; and
  2. Some other methods I defined in the class expect to give and receive non-null objects, so the handler function as typed in above is not useable for every promise or event handler.

An option would be to use async functions and do away with handler function variables, but that would go against the established code conventions my team set up for the project I had this dilemma in.

Therefore, I ask:

  1. Is there a better way to achieve my goal of keeping type safety in handler function variables in TypeScript?
  2. Does anybody know why the first snippet doesn't work?
  3. Any chance the first snippet may work someday (i.e. unionized function signature selection in the roadmap)?

Solution

  • To make your first snippet work, you need to treat handleThing as an initialized property and not a method; that means you give it a single type annotation and a single value. Note that the type of an overloaded function can be represented either as the intersection of each signature (e.g., ((x: string)=>number) & ((x: number)=>string)), or as a single object type with multiple bare function signatures (e.g., { (x: string): number; (x: number): string; }). Like this:

    class MyEntityService {
      private handleThing: {
        (a: undefined): undefined;
        <T extends object>(a: T): T;
      } = <T extends object>(a: T | undefined) => {
        if (!a) return undefined;
        // Various calls to other private methods here
        return a;
      }
    }
    

    The second snippet will work as you expect once you update to TypeScript 3.4 or above, since TypeScript 3.4 added better support for inferring generic types from uses of generic functions. In TypeScript 3.3 or below, the return value of bind() will have all the generics stripped out and replaced with any as you saw.

    Finally, I'm not sure why you don't go for a single signature:

    class MyEntityService {
      private handleThing = <T extends object | undefined>(a: T): T => {
        if (!a) return undefined as T; // have to assert here
        // Various calls to other private methods here
        return a;
      }
    }
    

    since (x: undefined) => undefined should match that signature if you let T range over both object and undefined.

    Anyway, hope that helps; good luck!