Search code examples
typescriptgenericsoverloading

Problem with extending an overloaded method in TypeScript


In a parent class, I define a method with different parameters: an index in a store or an element, and depending on the parameter, I get the element from the store by its index or just use the given element.
Thus, I use overloading to define these methods:

class Parent<T> {
  items: T[] = [];
  item: T = {} as T;

  applyMethod(index: number): void;
  applyMethod(item: T): void;
  applyMethod(param: number | T): void {
    if (typeof param === 'number') {
      param = this.items[param];
    }
    this.item = param;
  }
}

class Child1<T> extends Parent<T> {
  // In my application, I actually add an id: string overload.
  override applyMethod(param: number | T): void {
    // Do something more before calling:
    super.applyMethod(param);
  }
}

class Child2<T> extends Parent<T> {
  override applyMethod(param: number | T): void {
    if (typeof param === 'number') {
      super.applyMethod(param);
    } else {
      super.applyMethod(param);
    }
  }
}

See it in the TS Playground too.

In Child1, I have an error:

No overload matches this call.\
  Overload 1 of 2, '(index: number): void', gave the following error.\
    Argument of type 'number | T' is not assignable to parameter of type 'number'.\
      Type 'T' is not assignable to type 'number'.
  Overload 2 of 2, '(item: T): void', gave the following error.\
    Argument of type 'number | T' is not assignable to parameter of type 'T'.\
      'T' could be instantiated with an arbitrary type which could be unrelated to 'number | T'.(2769

I can workaround it as shown in Child2, but it is quite strange / stupid.

Another workaround would be to define applyMethod(param: any): void {} instead of number | T. It is classical and that's what I did initially, but I found out that TS only sees the param: any parameter definition, thus I lose typing.

I chose another way, calling super.applyMethod(param as any);. At least, the inputs keep their typings.

Is there another way, more elegant? Did I miss something?


Solution

  • Overloads in TypeScript have various limitations that make them hard to work with. The limitation you're running into is that currently the type checker only resolves calls to exactly one call signature. It will not attempt to combine call signatures.

    If you have two call signature like (x: string) => string and (y: number) => number, then you can call f("") or f(0), but not f(Math.random()<0.5?"":0), because the latter would require the compiler to combine both call signatures into something like (xOrY: string | number) => string | number. That is, it would need to synthesize call signatures corresponding to unions of the parameters of each call signature.

    There is an open feature request at microsoft/TypeScript#14107 asking for such support, but for now it's not part of the language.


    Until and unless that is implemented, you'll need to work around it. If you can avoid overloads entirely that is often the best approach, since there are multiple ways in which overloads are not well-supported by the type system (Typescript: ReturnType of overloaded function and many other examples can be found).

    If you do want to keep the overloads, then one of the workarounds (as mentioned in the GitHub issue) is to manually add an overload (at the bottom) that includes the relevant union:

    class Parent<T> {
      items: T[] = [];
      item: T = {} as T;
    
      applyMethod(index: number): void;
      applyMethod(item: T): void;
      applyMethod(param: number | T): void; // <-- add catch-all overload
      applyMethod(param: number | T): void {
        if (typeof param === 'number') {
          param = this.items[param];
        }
        this.item = param;
      }
    }
    

    Now the call inside the child class works as written, because it can be resolved to the third call signature from the parent:

    class Child1<T> extends Parent<T> {
      override applyMethod(param: number | T): void {
        super.applyMethod(param); // okay
      }
    }
    

    Playground link to code