Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calling abstract methods inside abstract class constructor #9209

Closed
prashaantt opened this issue Jun 16, 2016 · 23 comments
Closed

Calling abstract methods inside abstract class constructor #9209

prashaantt opened this issue Jun 16, 2016 · 23 comments
Assignees
Labels
Question An issue which isn't directly actionable in code

Comments

@prashaantt
Copy link

TypeScript Version:

1.8.10

Code

export abstract class AbstractClass {
    constructor() {
        this.abstractMethod();
    }

    abstract abstractMethod(): void;
}

Output

"use strict";
var AbstractClass = (function () {
    function AbstractClass() {
        this.abstractMethod();
    }
    return AbstractClass;
}());
exports.AbstractClass = AbstractClass;

Question

Is there a situation where calling this.abstractMethod inside the constructor makes sense? I have accidentally hit this pattern and the class compiles successfully, but the resulting JavaScript obviously fails because this.abstractMethod is not a function.

@blendsdk
Copy link

How about a base class where it needs to configure itself based on the actual implementation of de derived/consumer class.

@prashaantt
Copy link
Author

@blendsdk Yes, I was trying something similar, but the compiled JS fails to run so I don't think I got that right. Besides, the output looks buggy to me.

@blendsdk
Copy link

Here is an example: https://github.com/blendsdk/trunk/tree/master/abstract

>> tsc
>> node dist/ConsumerClass.js
>> Hey

@mhegazy
Copy link
Contributor

mhegazy commented Jun 16, 2016

the methods would work. properties would not. not sure if there is much we can do here though.

@aluanhaddad
Copy link
Contributor

How about a base class where it needs to configure itself based on the actual implementation of de derived/consumer class.

Isn't this dangerous? Isn't super being called before the derived class is fully initialized?

@mhegazy
Copy link
Contributor

mhegazy commented Jun 16, 2016

methods go on the prototype. so does not matter when you call the method, it should be there. properties are added to the instance after super is called in the constructor. so they will not be set.

the issue is we can prohibit the use of abstract properties in the constructor, and we probably should, but you can call a method from the constructor and the method tries to use a property, this we can not check.

@mhegazy mhegazy added the Bug A bug in TypeScript label Jun 16, 2016
@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jun 16, 2016

the issue is we can prohibit the use of abstract properties in the constructor, and we probably should, but you can call a method from the constructor and the method tries to use a property, this we can not check.

That is exactly what I meant. I was sort of saying that calling a derived method from a base class constructor is probably a bad idea. It is certainly discouraged in many languages. Perhaps it should be an error if a method marked as abstract is called from a constructor implementation within the same class.

Edit: I'm not sure it matters that the base class is marked abstract, just that the method is.

@prashaantt
Copy link
Author

prashaantt commented Jun 17, 2016

FWIW, I ran into this trying to make ConcreteClass have a specified constructor shape and staticMethod:

// abstract.ts

export abstract class AbstractClass {
    constructor(s: string) {
        this.abstractMethod(parseInt(s));
    }

    abstract abstractMethod(n: number): void;
}

// interface.ts

export interface Interface {
    new (s: string): AbstractClass;
    staticMethod(): void;
}

// concrete.ts

export class ConcreteClass extends AbstractClass {
    abstractMethod(n: number) { }
    static staticMethod() { }
}

export const concreteClass: Interface = ConcreteClass;

This works great in TS, but then emits that JS above. ConcreteClass works fine but AbstractClass fails to instantiate on its own.

I believe this is a valid requirement, but is there another way to achieve this?

@prashaantt
Copy link
Author

prashaantt commented Jun 17, 2016

By the way, I'm still new to TypeScript so the other thing I'm curious about here is, why can't the Language Service not automatically infer that my derived class (not declared abstract) needs to implement abstractMethod and offer me the IntelliSense for it with auto-complete? And why should I need to declare the argument types for abstractMethod again when they can also be automatically inferred from the declaration in the base class?

@blendsdk
Copy link

To my best knowledge these are handled by the IDE. If you use VSCode, it will complain about the abstract method not being implemented in the derived class. At the moment VSCode does not auto-generate code for the abstract function.

@prashaantt
Copy link
Author

it will complain about the abstract method not being implemented in the derived class

Yes, that it does. But perhaps it could go a step further in suggesting the method name and signature as well inside the derived class block.

@blendsdk
Copy link

Yes agreed. It is best to address these issues at VSCode. The typescript team cannot do much about it here I guess.

@aluanhaddad
Copy link
Contributor

To my best knowledge these are handled by the IDE. If you use VSCode, it will complain about the abstract method not being implemented in the derived class. At the moment VSCode does not auto-generate code for the abstract function.

VSCode does not do any code generation for TypeScript. It is all generated via the TypeScript compiler.

Furthermore, the TypeScript language service itself is responsible for producing errors in VSCode. Ide plugins like tslint can produce warnings. Neither effect code generation.

If you're using WebStorm, or Resharper in Visual Studio, JetBrains has their own language service but I would be shocked if the actual generated code was different.

@blendsdk
Copy link

Great insight! Thanks :)

@sandersn
Copy link
Member

@prashaantt can you explain a bit more about the errors and code gen you expected to see?

Specifically, when you say this:

ConcreteClass works fine but AbstractClass fails to instantiate on its own.

This is fine -- the compiler will stop you from instantiating AbstractClass so it doesn't matter that new AbstractClass("bar") doesn't work.

@sandersn
Copy link
Member

About your other question, I believe Quick Fixes are coming to Visual Studio soon, which means that the API will be available to other editors. Quick Fixes is the VS name for things like suggesting a method implementation of an abstract method.

It looks like VSCode supports quick fixes in C#, so it should be pretty easy to add them for TypeScript once they are done.

@mhegazy
Copy link
Contributor

mhegazy commented Jun 17, 2016

@sandersn, the bug here, i believe, is the compiler allowing access of abstract proprieties in the base constructor. these are guaranteed to fail at runtime, no matter what.

@sandersn
Copy link
Member

I think @prashaantt's question is about methods (at least principally). I created a separate bug for accessing abstract properties in the constructor: #9230.

@prashaantt
Copy link
Author

prashaantt commented Jun 17, 2016

can you explain a bit more about the errors and code gen you expected to see?

The reason I want to enforce the above contract (specific constructor and staticMethod, while deferring the core implementation inside abstractMethod which therefore must be implemented by each derived class) is a requirement of the server framework I'm using. So to follow along the earlier code, it attempts to call the named export concreteClass directly and fails spectacularly:

const ConcreteClass = require('./concrete.js');
ConcreteClass.concreteClass("1"); // TypeError: this.abstractMethod is not a function

I erroneously reported earlier that the following works, but it's a completely different thing and not what the framework actually does with the ConcreteClass.

const ConcreteClass = require('./concrete.js');
new ConcreteClass.concreteClass("1"); // this works

I can avoid using abstractMethod but will then lose much of the benefits of having a fully enforceable contract in TypeScript, which is what my experiment was all about with this use case.


So @sandersn at this point my problem might be very specific, but I still think the compiler should probably warn me against calling abstractMethod. #9230 should clear the issue somewhat (I was unaware that 1.8+ now supports abstract properties), but accessing abstract methods from the constructor is potentially bad and should perhaps also be discouraged.

@prashaantt
Copy link
Author

prashaantt commented Jun 17, 2016

In fact, I've been able to get closer to my desired behaviour with the new abstract properties feature, when used a function:

// abstract.ts

export abstract class AbstractClass {
    constructor(s: string) {
        this.abstractMethod(parseInt(s));
    }

    abstract abstractMethod: (n: number) => void;
}

// concrete.ts

export class ConcreteClass extends AbstractClass {
    abstractMethod = (n: number) => { };
    static staticMethod() { };
}

export const concreteClass: Interface = ConcreteClass;

The output from concrete.ts now looks like this:

// concrete.js

...
var ConcreteClass = (function (_super) {
    __extends(ConcreteClass, _super);
    function ConcreteClass() {
        _super.apply(this, arguments);
        this.abstractMethod = function (n) { };
    }
    ConcreteClass.staticMethod = function () { };
    return ConcreteClass;
}(abstract_1.AbstractClass));
...

And this would work perfectly if only this.abstractMethod = function (n) { }; appeared before the _super.apply(this, arguments);.

@sandersn
Copy link
Member

sandersn commented Jun 17, 2016

It sounds to me that your problem is not with abstract but with construct signatures versus call signatures. Your problem still arises with normal overriding methods:

// abstract.ts
class Class {
    constructor(s: string) {
        this.method(parseInt(s));
    }

    method(n: number): void {
        alert("don't call me!");
    }
}

// concrete.ts

class ConcreteClass extends Class {
    method(n: number) { alert(n) }
    static staticMethod() { }
}

let call = ConcreteClass('1'); // error!
let cons = new ConcreteClass('1');

The question is why you don't get an error on let call = ConcreteClass(). Is it the library that is calling your concreteClass.ConcreteClass ? If that's the case, you should probably export a function that calls your constructor:

export function concreteClass(n: number) {
  return new ConcreteClass(n);
}

If you need other functions nested on that function, then you can do:

function create(n: number) {
  return new ConcreteClass(n);
}
export const concreteClass = create as Interface;
concreteClass.staticMethod = function() { };

@mhegazy mhegazy added Question An issue which isn't directly actionable in code and removed Bug A bug in TypeScript labels Jun 17, 2016
@prashaantt
Copy link
Author

Is it the library that is calling your ConcreteClass.concreteClass

That's right, that's their contract.

Unless I misunderstood you, the following doesn't compile up front:

export const concreteClass = create as Interface;
// `Property 'staticMethod' is missing in type '(s: string) => ConcreteClass'`

I removed the static check from Interface to fix that error, but then couldn't get this to compile either:

concreteClass.staticMethod = function() { };
// `Property 'staticMethod' does not exist on type '(s: string) => ConcreteClass'`

Thanks for your time everyone. I guess I'll have it give this up for now and maybe try again later.

@sandersn
Copy link
Member

You have to change Interface too, so that it uses a call signature instead of a construct signature:

interface Interface {
    (): AbstractClass; // no *new* here.
    staticMethod(): void;
}
function create() { 
  // same as before ...
};
function f() { }
const concreteClass = create as Interface;
concreteClass.staticMethod = f;

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants