3

I’m currently working on a project in TypeScript (version 2.9.2), and have encountered an unexpected polymorphic behavior. In the likes of Java and C#, interfaces define polymorphic behavior as much as classes do—that is, in the following, it is just as true that item1 can be of type A as it is that it can be of type B, and that item2 can be of type C as it is that it can be of type D:

interface A { }
class B implements A { }
class C { }
class D extends C { }

But in TypeScript, this seems to not be the case. I have approximately the following setup:

interface A {
    new (str: string): Module;
    someFunction(str: string): A;
}

class B implements A {
    constructor(str: string) { /* ... */ }
    someFunction(str: string): B { /* ... */ }
}

The compiler seems to have an issue with the return type of B’s someFunction(), but by my understanding of polymorphism, since B implements A, if a function returns something of type A, then it should also be able to return something of type B. That being said, it doesn’t make sense that something should “be of type A”, since interfaces can’t be instantiated, and are no more than an intangible agreement or contract between classes. It then seems reasonable that if A were to instead be an abstract class, the polymorphic behavior should behave as I expect—which it indeed does—but in the scope of the library I’m building, it seems more proper for A to be an interface.

The issue that the compiler gives in particular is as follows on the line that declares B's someFunction():

[ts]
Property 'someFunction' in type 'B' is not assignable to the same property in base type 'A'.
  Type '(str: string) => B' is not assignable to type '(str: string) => A'.
    Type 'B' is not assignable to type 'A'.
      Types of property 'someFunction' are incompatible.
        Type '(str: string) => B' is not assignable to type '(str: string) => A'.
(method) Project.B.someFunction(str: string): B

Part of the problem seems to lie in the fact that I declare a constructor in A. If I delete that constructor definition, the problem is resolved, but I need that definition to be part of the agreement of what it fundamentally means to be of type A.

Given the polymorphic behavior I expect, how might I go about writing my interface, or should I be using an abstract class instead? How do I bring this polymorphic behavior about?

jmindel
  • 465
  • 1
  • 7
  • 16
  • which version are you using? It looks fine to me [here](http://www.typescriptlang.org/play/#src=interface%20A%20%7B%0D%0A%20%20%20%20someFunction()%3A%20A%3B%0D%0A%7D%0D%0A%0D%0Aclass%20B%20implements%20A%20%7B%0D%0A%20%20%20%20someFunction()%3A%20B%20%7B%20return%20new%20B()%3B%20%7D%0D%0A%7D). And this approach seems fine to me. – mrmcgreg Jul 04 '18 at 00:43
  • You mentioned "The compiler seems to have an issue". Can you post the specific error that it generated? – CRice Jul 04 '18 at 00:52
  • @CRice I've added the error in my post. Let me know if that sheds any further light on the odd behavior. – jmindel Jul 07 '18 at 04:29
  • @mrmcgreg Thank you for pointing out the version--turns out that an update (I was on 2.4.2 before) was enough to fix a few things. The setup is a bit more like [this](http://www.typescriptlang.org/play/#src=interface%20A%20{%20%20%20%20new(str%3A%20string)%3A%20A%3B%20%20%20%20someFunction()%3A%20A%3B}class%20B%20implements%20A%20{%20%20%20%20constructor(str%3A%20string)%20{%20}%20%20%20%20someFunction()%3A%20B%20{%20return%20new%20B()%3B%20}}) (updated the post to reflect it as well), and the issue seems to be in my declaration of a constructor in `A` instead. – jmindel Jul 07 '18 at 04:47

1 Answers1

1

I need that definition to be part of the agreement of what it fundamentally means to be of type A

Unfortunately there is no support for that in the language. Construct signature can not be a part of contract that a class is declared to implement. extends declares only the instance part of the contract, constructors and static methods are part of so-called "static part" and there is no way to declare a contract for that.

TypeScript uses structural typing so you can in fact use B whenever an interface that specifies a construct signature is expected, but that interface must be declared separately, and the conformance will be checked every time B is used in-place, there is no way to declare it beforehand:

interface AConstructor {
    new (str: string): A;
}

interface A {
    someFunction(str: string): A;
}

class B implements A {
    constructor(str: string) { /* ... */ }
    someFunction(str: string): B { /* ... */ }
}

function f(cls: AConstructor) {
    const instance = new cls('hi');
    instance.someFunction('how are you?');
}

f(B);  // ok
artem
  • 46,476
  • 8
  • 74
  • 78
  • Understood. Thank you so much! I realized after updating my question that the TypeScript Handbook had some material on this, but your answer made it much clearer. For the time being, I've switched to an abstract class instead to enforce a few different typing requirements that I've realized make more sense to emphasize. – jmindel Jul 10 '18 at 21:08