2

I don't know if it's possible to do what I pretend to do but here it goes as a quick example ...

A simple class, ABC, with an overloaded constructor using optional parameters:

export class ABC {

s:string = "";
n:number = 0;

constructor();
constructor(_s:string);
constructor(_n:number);

constructor(_arg?:string|number) {

    if (typeof _arg == "string") {
        this.s = <string>_arg;
    } else if (typeof _arg == "number") {
        this.n = <number>_arg;
    } else {
        console.log("no args!");
    }
}

}

  • Then another one class, DEF, inheriting from ABC:

    export class DEF extends ABC {

      constructor();
      constructor(_s:string);
      constructor(_n:number);
      constructor(_arg?:string|number) {
          super(_arg);
      }
    

    }

  • Typescript complains about DEF using super(_arg) with the following group of error messages:

No overload matches this call.

Overload 1 of 3, '(_s: string): ABC', gave the following error. Argument of type 'string | number' is not assignable to parameter of type 'string'. Type 'number' is not assignable to type 'string'.

Overload 2 of 3, '(_n: number): ABC', gave the following error. Argument of type 'string | number' is not assignable to parameter of type 'number'.

Type 'string' is not assignable to type 'number'.ts(2769) ABC.ts(10, 2): The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.

How can I make DEF to inherit from ABC using its constructor with the optional arguments? Can we make the "overloads visible from the exterior" as mentioned at the last line?

Many thanks for your help!

-Francisco

Following jcalz suggestion (thanks!) here it is a bit more elaborate example with the reason I've posed the original question:

export class ABC {

    constructor();
    constructor(_arg1:string);
    constructor(_arg1:number);
    constructor(_arg1:string, _arg2:number);
    constructor(_arg1:string, _arg2:string);
    constructor(_arg1:number, _arg2:string);

    // DO NOT ALLOW THE FOLLOWING!!!
    // constructor(_arg1:number, _arg2:number);

    // but here it says otherwise ...
    constructor(_arg1?:string|number, _arg2?:string|number);

    // ----------------------------------------------------
    constructor(_arg1?:string|number, _arg2?:string|number) {
    }
}

export class DEF extends ABC {

    constructor(_arg1?:string|number, _arg2?:string|number) {
        super(_arg1,_arg2);
    }
}

This one allows us to build ABC(1,2) something it shouldn't be able to do, while checking it at compile time ... There will be some combinations of types that shouldn't be accepted by the constructor(s). How can it be done?

5
  • 1
    You can add another call signature corresponding to the implementation, as shown here, but I don't understand why you would use overloads at all in an example like this (as shown at the fix on the bottom of that link); could you edit the example so there's some kind of motivation for the overloads? Something that couldn't be better accomplished by a normal constructor? Commented Jul 4, 2023 at 0:11
  • You are right about the example. It's a very simple cut-down version of a geometry program where a 3-dimensions Point3D inherits from a Point2D with several default values if the parameters are not given. To avoid copying a large bunch of code I've simplified it to this. Of course, ABC could be much simpler, it is only used here as an illustration ;-) Commented Jul 4, 2023 at 0:29
  • Okay well I'll write up an answer but I'm going to have to mention that the example is too simple to warrant using overloads. It would be really helpful if you could make it demonstrate the need for them instead. Commented Jul 4, 2023 at 0:32
  • So then does this version work just as well? Still using unions instead of overloads. Let me know and I'll write up an answer. Commented Jul 4, 2023 at 0:56
  • Ok! You've got it :-) Of course it works, many thanks! But could it had been done with overloads? Or was it the wrong path from the very beginning? Commented Jul 4, 2023 at 1:14

1 Answer 1

2

Overloads in TypeScript consist of a set of call signatures, which are visible from the outside:

// call signatures
function foo(x: string): void;
function foo(x: number): void;

They may also be implemented, and the implementation will also have a signature, which needs to be wide enough to support all the call signatures. But this implementation signature is invisible from the outside:

// implementation
function foo(x: string | number) { }

foo("x"); // okay
foo(1); // okay
foo(Math.random()<0.5 ? "x" : 1); // error

This is intentionally the way overloads are. You might think that just having the two call signatures is sufficient, because if string or number are accepted separately, then string | number should be accepted also. There's a longstanding open feature request for such support, at microsoft/TypeScript#14107, but it's not part of the language.

That means if you have an overloaded function implementation bar() calling another overloaded function foo(), the implementation of bar will only see the call signatures of foo, and it's possible for it to fail:

function bar(x: string): void;
function bar(x: number): void;
function bar(x: string | number) {
    foo(x); // error! there is no overload of foo() accepting string | number
}

If you need overloads and you need to support the implementation signature as a call signature, then you'll need to add it explicitly:

// call signatures
function foo(x: string): void;
function foo(x: number): void;
function foo(x: string | number): void; // okay
function foo(x: string | number) { }
foo("x"); // okay
foo(1); // okay
foo(Math.random() < 0.5 ? "x" : 1); // okay

function bar(x: string): void;
function bar(x: number): void;
function bar(x: string | number) {
    foo(x); // okay
}

But of course, in this example, you could use only that added call signature and get rid of the other ones, because it subsumes them:

function foo(x: string | number) { }
foo("x"); // okay
foo(1); // okay
foo(Math.random() < 0.5 ? "x" : 1); // okay
function bar(x: string | number) {
    foo(x); // okay
}

It is possible that you still need what amounts to separate call signatures, as shown in your more complicated example. In that case, you can sometimes actually still get away with a single call signature, if all of the signatures you want return the same type. For a constructor that happens automatically (they all construct the same type), so that works here.

The way you can do it is by representing each call signature's parameter list as a rest parameter of a tuple type and then make the single call signature take the union of those rest tuples:

type CtorArgs =
    [arg1?: string | number, arg2?: undefined] |
    [arg1: string, arg2: string | number] |
    [arg1: number, arg2: string]

export class ABC {
    s: string = "";
    n: number = 0;
    constructor(...[arg1, arg2]: CtorArgs) { }
}

export class DEF extends ABC {
    constructor(...args: CtorArgs) {
        super(...args);
    }
}

new ABC(); // okay
new ABC(1); // okay
new ABC(""); // okay
new ABC("", ""); // okay
new ABC("", 1); // okay
new ABC(1, ""); // okay
new ABC(1, 1); // error

That works as expected!


Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.