Skip to content

Suggestion: abstract classes #6

Description

Support an abstract keyword for classes and their methods

Examples:

abstract class Base {
    abstract getThing(): string;
    getOtherThing() { return 'hello'; }
}
var x = new Base(); // Error, 'Base' is abstract

// Error, must either be 'abstract' or implement concrete 'getThing'
class Derived1 extends Base { }

class Derived2 extends Base {
    getThing() { return 'hello'; }
    foo() { super.getThing(); } // Error: cannot invoke abstract members through 'super'
}
var x = new Derived2(); // OK
var y: Base = new Derived2(); // Also OK
y.getThing(); // OK
y.getOtherThing(); // OK

abstract class Empty { } // OK

Activity

  1. am11 commented on Jul 27, 2014

    @am11

    👍

    The example would be more awesome if you incorporate virtual as well. 😄

    abstract class Base {
        abstract getThing(): string;
        getOtherThing() { return 'hello'; }
        virtual getVirtualThing() { return 'hello virtual'; }
    }
    ...
  2. DanielRosenwasser commented on Jul 27, 2014

    @DanielRosenwasser
    Member

    Adeel Mujahid (@am11) given that methods in JS get called by traversing the prototype chain, all methods are already "virtual" in the traditional sense that C++, C#, Java, and similar languages define it.

    For instance:

    class E { 
        f () { alert("E"); }
    }
    
    class F extends E {
        f () { alert("F"); }
    }
    
    var x: E = new F();
    x.f();

    will alert "F".

    Did you have something else in mind?

  3. am11 commented on Jul 27, 2014

    @am11

    Daniel Rosenwasser (@DanielRosenwasser), that is so true. Thanks!

    Actually, I was thinking about the conventional OO keywords virtual and overrirde, so its easy to clearly differentiate between various inheritance behaviors. The TS compiler might just throw warning if those keywords are missing in applicable scenarios and fallback to standard JS convention.

    I believe this way we can bring C#'s new modifier as well:

    abstract class A {
        public void foo() { }
        protected virtual void bar() { }
    }
    
    class A1 : A {
        protected override void bar() { }
    }

    there may exist A2, such that:

    class A2 : A {
        public new void foo() { }
    }

    to explicitly hide super's implementation (without new keyword there, CS compiler gives warning).

  4. RyanCavanaugh commented on Jul 28, 2014

    @RyanCavanaugh
    MemberAuthor

    new is infeasible given JavaScript's runtime semantics; given that we've already shipped the language this way, virtual would be the default in the absence of any other modifier. Let's take discussion of those modifiers to a different suggestion if needed.

    Seems like the example above has enough information to discuss. Some open questions:

    • Can I have an abstract class with zero abstract methods?
    • Is the abstract modifier implicit in a class that doesn't implement all abstract methods?
  5. am11 commented on Jul 29, 2014

    @am11

    Can I have an abstract class with zero abstract methods?

    Since abstract class -- in principle -- serves a purpose of grouping commonalities together such that its objects alone don't make sense; I think we should rule no_abstract_method as a legit case and let the TypeScript linter take care of recommending best practices, code-styling et el.

    Is the abstract modifier implicit in a class that doesn't implement all abstract methods?

    Given an abstract class would be able to inherit another abstract class, implicit inference like this would be confusing? I guess (at least C#/Java) developers would expect compiler to throw error and editor to help them correct it, if the first non-abstract / concrete implementer is missing any expected override. Well as a Ruby guy, I'd probably appreciate this kind of crypto-magic :`)

  6. RamIdeas commented on Oct 23, 2014

    @RamIdeas

    Even before abstract is implemented, I'm still hoping you'd allow intellisense kick in (in Visual Studio) when overriding base class functions:

    class BaseClass {
      myFunction() { /* TO BE OVERRIDDEN */ }
    }
    class MyClass extends BaseClass {
      myF // intellisense should have kicked in -- but it doesn't
    }

    My only option is to open BaseClass.ts to check I'm spelling it correctly.

  7. RamIdeas commented on Oct 23, 2014

    @RamIdeas

    I'd still prefer to see more descriptive keywords, as I mentioned on https://typescript.codeplex.com/discussions/449920

    C# VB (on classes) VB (on methods)
    abstract MustInherit MustOverride
    virtual Overridable Overridable
    sealed NotInheritable NotOverridable

    From these more descriptive keywords, it can be seen immediately that the overridable keyword is unnecessary as methods in JavaScript are overridable by default as opposed to in C# and is less likely to lead to confusion demonstrated by Adeel Mujahid (@am11). I believe the same gain in clarity is achieved with the other keywords listed.

  8. RyanCavanaugh commented on Dec 3, 2014

    @RyanCavanaugh
    MemberAuthor

    We're trying to clear out our design backlog and I think this should come up soon once ES6 features are wrapped up. We discussed abstract in a design meeting a few months ago and there didn't seem to be any big open questions.

  9. metaweta commented on Dec 12, 2014

    @metaweta

    The standard design pattern in JS is to throw an exception in the abstract class:

    function Base() { throw new Error("Not implemented."); }
    Base.prototype.f = function () { throw new Error("Not implemented."); };
    Base.prototype.g = function () { /* default impl */ };
    
    function Derived() {}
    Derived.prototype.f = function () { /* impl */ };
    Derived.prototype.g = function () { /* override impl */ };
    

    The benefit TypeScript could provide is reducing boilerplate and a compile-time error.

  10. RyanCavanaugh commented on Dec 12, 2014

    @RyanCavanaugh
    MemberAuthor

    Discussed today in the design meeting.

    The open question here is how we prevent you from trying to instantiate an abstract class. Consider some code:

    abstract class Abs {
      constructor() { /* some general init here */ }
    }
    
    var x = new Abs(); // Desired: error

    This case is easy -- when you invoke new on a symbol, we see if the symbol is a class with abstract, and error if so.

    Next:

    var a = Abs;
    var x = new a(); // Not a good thing to do

    This is less obvious. You probably want an error? But then consider this case:

    class B extends Abs { /* ... */ }
    class C extends Abs { /* ... */ }
    
    var t: typeof Abs;
    if(/*... */) {
      t = B;
    } else {
      t = C;
    }
    var j = new t(); // This is fine

    There's nothing distinguishing this case from the previous one (in terms of the type system), but the latter is perfectly valid code to write (and would probably even be common code to write in a factory method). We will probably have to not error on the new a(); case, both because it mimics a valid pattern and because attempting to prevent it would require a lot of extra type system mechanics.

  11. Vadorequest commented on Dec 12, 2014

    @Vadorequest

    What about an hidden field?

    Let's say we have a class

    abstract class Abs {
      constructor() { /* some general init here */ }
    }
    

    Because the class is abstract, we cannot instanciate it, you said you wanted to check if the class was abstract or not to do so. What if having a abstract class would generate somehow hidden field ___abstract___ = true. If the class isn't abstract like with class B extends Abs { /* ... */ } then we would have the ___abstract___ = false, so to check if a class is abstract or not we could just check that hidden field.

    I don't know if it's a proper solution, or even if it's possible, but the point is to check if the closest class is abstract or not, whatever if it extends and abstract class or not.

  12. 35 remaining items

  13. DickvdBrink commented on Apr 28, 2015

    @DickvdBrink
    Contributor

    For the record, I'm currently (trying) to implement this.

    edit What is the best way for this? First some basic stuff and create a PR for review? Or implement it with full test-suite and stuff? Because when I'm moving in total wrong direction I might waste a lot of my time and maybe yours too.

  14. mhegazy commented on Apr 28, 2015

    @mhegazy
    Contributor

    We are open to looking at incremental changes. Just make sure the first iteration is substantial enough to warrant feedback.

    I would recommend sharing your approach and design on this issue before jumping into implementation. this way we can help save you time early on.

  15. jeffreymorlan commented on Apr 29, 2015

    @jeffreymorlan
    Contributor

    Indirect invocations, e.g. var x = MyAbstractClass; var y = new x(); are allowed, and the static side of abstract classes have the same construct signatures they would have if they weren't abstract

    I think this should be reconsidered. Special-casing only a direct "new MyAbstractClass" (rather than removing abstract classes' new() signatures) is hacky, and makes it easy to accidentally pass an abstract class to a function that instantiates it (function constructAndDoOtherStuff(clazz: new() => Foo) { ... })

    Earlier, this code was given as an example in favor of abstract classes having a new() signature:

    class B extends Abs { /* ... */ }
    class C extends Abs { /* ... */ }
    
    var t: typeof Abs;
    if(/*... */) {
      t = B;
    } else {
      t = C;
    }
    var j = new t(); // error here if `typeof Abs` is not instantiable
    

    But t could be declared as new() => Abs instead, which better describes what is expected of a valid t value anyway. (There is no backward compatibility to worry about, since there are no abstract classes in existing TypeScript code.)

    In fact, using typeof Abs here may already be an error even without abstract! Suppose that Abs has an extra constructor parameter, which B and C supply in their super(...) calls (This is a pattern I use a lot - currently, it's the only way for a class to have an abstract-like "hole" that must be supplied by subclasses):

    class Abs { constructor(public typeName: string) {} }
    class B extends Abs { constructor() { super("Class B"); } }
    class C extends Abs { constructor() { super("Class C"); } }
    
    var t: typeof Abs;
    t = B;
    new t(); // Error here - wrong number of parameters
    

    Because Abs has the wrong constructor signature, we are already required to use new() => Abs instead of typeof Abs in this case. It doesn't seem an undue burden to require the same if Abs's "holes" take the form of abstract methods rather than constructor parameters.

  16. Griffork commented on Apr 29, 2015

    @Griffork

    jeffreymorlan there was a suggestion that users can program their abstract classes to error in the constructor if they wanted it to (it's above) like this:

    class classa {
        constructor () {
            if(Object.getPrototypeOf && (Object.getPrototypeOf(this) === classa.prototype) {
                throw new error("classa is abstract and should not be directly instantiated");
            } 
        } 
    } 

    This specifically checks of classa is 'bottom' of the prototype chain.

  17. added
    CommittedThe team has roadmapped this issue
    FixedA PR has been merged for this issue
    and removed on Jul 1, 2015
  18. RyanCavanaugh commented on Jul 1, 2015

    @RyanCavanaugh
    MemberAuthor

    Merged into master and will be available in our 1.6 release. Thanks Arthur Ozga (@aozgaa) !

  19. ddotlic commented on Jul 2, 2015

    @ddotlic

    Yeah, thanks Arthur Ozga (@aozgaa)! Lately, I've been adding many, many throw new Error('this should not have been called, it's abstract') across my codebase with the intention of replacing it all with proper abstract support.

    Beta/Alpha 1.6 cannot come soon enough 😉

  20. aozgaa commented on Jul 2, 2015

    @aozgaa
    Contributor

    My apologies about not offering the back-reference. Thanks @jasonwilliams200OK for linking above!

  21. locked and limited conversation to collaborators on Jun 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CommittedThe team has roadmapped this issueFixedA PR has been merged for this issueSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions