microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.16k stars 12.38k forks source link

Support override keyword on class methods #2000

Closed rwyborn closed 3 years ago

rwyborn commented 9 years ago

(Update by @RyanCavanuagh)

Please see this comment before asking for "any updates", "please add this now", etc.. Comments not meaningfully adding to the discussion will be removed to keep the thread length somewhat reasonable.


(NOTE, this is not a duplicate of Issue #1524. The proposal here is more along the lines of the C++ override specifier, which makes much more sense for typescript)

An override keyword would be immensely useful in typescript. It would be an optional keyword on any method that overrides a super class method, and similar to the override specifier in C++ would indicate an intent that "the name+signature of this method should always match the name+signature of a super class method". This catches a whole range of issues in larger code bases that can otherwise be easily missed.

Again similar to C++, it is not an error to omit the override keyword from an overridden method. In this case the compiler just acts exactly as it currently does, and skips the extra compile time checks associated with the override keyword. This allows for the more complex untyped javascript scenarios where the derived class override does not exactly match the signature of the base class.

Example use

class Animal {
    move(meters:number):void {
    }
}

class Snake extends Animal {
    override move(meters:number):void {
    }
}

Example compile error conditions

// Add an additional param to move, unaware that the intent was 
// to override a specific signature in the base class
class Snake extends Animal {
    override move(meters:number, height:number):void {
    }
}
// COMPILE ERROR: Snake super does not define move(meters:number,height:number):void

// Rename the function in the base class, unaware that a derived class
// existed that was overriding the same method and hence it needs renaming as well
class Animal {
    megamove(meters:number):void {
    }
}
// COMPILE ERROR: Snake super does not define move(meters:number):void

// Require the function to now return a bool, unaware that a derived class
// existed that was still using void, and hence it needs updating
class Animal {
    move(meters:number):bool {
    }
}
// COMPILE ERROR: Snake super does not define move(meters:number):void

IntelliSense

As well as additional compile time validation, the override keyword provides a mechanism for typescript intellisense to easily display and select available super methods, where the intent is to specifically override one of them in a derived class. Currently this is very clunky and involves browsing through the super class chain, finding the method you want to override, and then copy pasting it in to the derived class to guarantee the signatures match.

Proposed IntelliSense mechanism

Within a class declaration:

  1. type override
  2. An auto complete drop down appears of all super methods for the class
  3. A method is selected in the drop down
  4. The signature for the method is emitted into the class declaration after the override keyword.
kungfusheep commented 8 years ago

override could potentially be used on fields, if they're being re-declared in the subclass body. Optional interface methods aren't overrides so are outside of the scope of what we're talking about here.

On Thu, 14 Apr 2016 at 11:58, Olmo notifications@github.com wrote:

What about setting inherited fields vs redefining new ones? React state is a good example.

Or implementing optional interface methods?

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/2000#issuecomment-209879217

BernieSumption commented 8 years ago

Optional interface methods aren't overrides so are outside of the scope of what we're talking about here.

Optional interface methods are a concept fairly unique to TypeScript as far as I know, and I think a TypeScript implementation of "overrides" should apply to them to.

In the case of React lifecycle methods like componentDidMount, these are optional interface methods that have no implementation in the superclass.

kungfusheep commented 8 years ago

In the case of React lifecycle methods like componentDidMount, these are optional interface methods that have no implementation in the superclass.

Exactly, so they don't override anything. I think we're getting confused about what the override keyword is intended to provide here, because if you're just looking for a way of getting better code hinting/intellisense there are other ways that can be achieved without adding new keywords to the language.

armandn commented 8 years ago

Guys, with all due respect, can we please stay focused. I think that discussing alternative keywords is counterproductive, especially since the Issue was started with a specific request.

Can we all either agree that we need override and ask nicely the Typescript team to add it or drop the request and let the Issue closed?

joewood commented 8 years ago

I think it's always useful to pose the request in the context of what the problem is, there's always different ways to solve the problem. The sticking point with override was around it being mandatory or not (some making the point that it isn't mandatory in C++). The problem a lot of people have referenced is getting the name right for overridable functions (which may or may not be implemented in the super class) - React's lifecycle functions being the main problem.

If override doesn't work, then maybe we should close this issue and open a more general point about this problem. The general problem here is that the interface contract with a superclass is untyped and unchecked, and that's tripping up developers and it would be great if TS or the tooling can help.

olmobrutall commented 8 years ago

@armandn I don't think our lack of closure or the niceness of our suggestion is what's keeping the request rejected, but the differences between C# and TS semantics:

C# base method override:

C# interface implementation:

So the behavior in C# are quite different depending if you ask about classes or interfaces, but in any case you get the main three benefits:

  1. Checking for identical signature
  2. Checking that the method can/should be overridden (misspelling in method name)
  3. IDE support writing the function

With slightly different semantics, we already have 1) in TS, unfortunately 2) and 3) are missing. The reason override was rejected, in my opinion, is that a similar syntax assumes a similar behavior, and this not desirable because:

A class with 5 methods, of which 3 are marked override, wouldn't imply that the other 2 aren't overrides.

It happens that we already have 1) 2) and 3) when writing object literals, but not when writing class members that extend other classes or implement interfaces.

With this in mind I think we can all agree on this semantics:

Additionally, I think this two are necessary for completeness of the solution*:

This two points are useful in the very-real use case of React component implementation:

class MyComponent<MyComponentProps, MyComponentState> {
    implements.state = {/*auto-completion for MyComponentState here*/};

    implements.componentWillMount(){
        //do my stuff
    }
}

The Annotation-based solution from @RyanCavanaugh is not enough because:

Given the semantics, it's just about choosing the right syntax. Here some alternatives:

Opinions?

joewood commented 8 years ago

@olmobrutall good summary. Couple of points:

One keyword option (taken from C#) would be new - indicating a new slot:

class MyComp extends React.Component<IProps,IState> {
...
    new componentWillMount() { ... }
    componentWillMount() { ...} // would compile, maybe unless strict mode is enabled
    new componentwillmount() { ... } <-- error

My other point is the issue with mandatory nature of using the above. As a contract between the parent super class and the derived class it makes sense to specify what are interface points where the above syntax would be valid. These are really internal extensions points for the super class, so something like:

class Component<P,S> {
    extendable componentWillMount() {...}
}

The same would apply to the interface too.

olmobrutall commented 8 years ago

Thanks :)

What about just writing this?

class MyComponent<MyComponentProps, MyComponentState> {
    this.state = {/*auto-completion for MyComponentState here*/};

    this.componentWillMount(){
        //do my stuff
    }
}

About extendable, there's already abstract in TS 1.6, and adding virtual will maybe create again the split world problem?

joewood commented 8 years ago

Right, I thought about abstract but there may be an implementation in the super class, so it doesn't really make sense. Likewise with virtual, as that would imply non-virtual members are not virtual - which is misleading.

this. works, I guess you could even have (as a long form):

   this.componentWillMount = () => { }

The only issue with this is that it should only be scoped to earmarked extension points, not all members of the base class.

kungfusheep commented 8 years ago

what is going on...

TypeScript isn't and never has been the javascript version of C#. So the reason isn't because the suggested functionality differs from the semantics of C#. The reasons for closure as stated by Ryan and then clarified by Daniel R were

As Ryan explained, the issue is that marking a method as an override doesn't imply that another method isn't an override. The only way that it would make sense is if all overrides needed to be marked with an override keyword (which if we mandated, would be a breaking change).

Yet you're still persisting with your problems around autocomplete. Autocomplete doesn't need a new keyword in the language to provide you with improved suggestions.

The reason this thread exists was to get compiler errors when a function is illegally overridden or when a method was declared to be an override but didn't actually override a function in a base class. It's a simple and well defined concept across many languages that also support most if not more language features typescript has to offer. It doesn't need to solve all the worlds problems, it just needs to be the override keyword, for overrides.

More people have since shown interest in the original proposal, so lets stick to the topic the issue was raised for and raise new issues for new ideas.

olmobrutall commented 8 years ago

TypeScript isn't and never has been the javascript version of C#.

I'm comparing it with C# as a reference language, since I assume this is the behavior you guys are assuming.

Autocomplete doesn't need a new keyword in the language to provide you with improved suggestions.

How you suggest it to be triggered then? If you show a autocomplete combobox when writing a random name in a class context will be very annoying when we just want to declare a new field or method.

The reason this thread exists was to get compiler errors when a function is illegally overridden or when a method was declared to be an override but didn't actually override a function in a base class.

I absolutely included this use case, under Benefit 2.

It doesn't need to solve all the worlds problems, it just needs to be the override keyword, for overrides.

So your suggestion is to fix one step at a time instead of moving one step back and look at similar/related problems?. That maybe a good idea for your Scrum Board but not for designing languages.

The reason they are conservative about adding the keyword is that they can not remove features from a language.

Some design mistakes in C# because of lack of completion:

Just try to use React for a second and you'll see the other side of the picture.

kungfusheep commented 8 years ago

Overriding a method that's already been implemented in a base class and implementing an interface method are two completely different things. So yes, what's being suggested is to fix one of those scenarios with a dedicated keyword instead of trying to come up with some Swiss-Army keyword.

What good is a keyword that can mean any one of 3 different things to developers reading some code for the first time? It's ambiguous and confusing, especially if you're talking about using a keyword like this which already does other (totally unrelated!) things in the language - it couldn't be more generic, it's almost useless.

If your main concern is auto-complete, editors have enough information now to be able to suggest methods from base classes and implemented interfaces 'as you type'.

BernieSumption commented 8 years ago

Overriding a method that's already been implemented in a base class and implementing an interface method are two completely different things.

In the general case yes, but we're not talking about implementing any interface method. We're talking about an optional interface method where the parent class implements the interface. In this case you can say that 1) interface permits the method to be implemented as undefined, 2) the parent class has an undefined implementation, and 3) the child class is overriding the undefined implementation with a method implementation.

kevmeister68 commented 8 years ago

@olmobrutall I think your comment about designing languages and how it is not a scrum board is a little self-serving. I have seen about four updates to TS in the space of under a year.

If the language design was as well considered as you imply it should be, then there would already be a language spec document telling us exactly how overrides should work, and we mightn't even be having this conversation.

I don't make this comment with any denigration of the TS developers/designers, because TS is already excellent and I would dread having to use standard JS instead.

Yes TS is not C# and it's not C++. But a number of languages have chosen the override keyword to meet the objectives discussed here so it seems counterproductive to suggest totally alien syntax.

The chief issue seems to be not wanting to introduce a breaking change. The simple answer is a compiler flag, end of story. For some people like me, an optional override keyword is useless. For others they want to embellish their code incrementally. Compiler flag solves the dilemma.

Signature differences are a different conversation. The new keyword appears unnecessary because JS can't support multiple methods of the same name (unless TS creates signature-derived mangled names a'la C++, which is highly unlikely).

olmobrutall commented 8 years ago

I have seen about four updates to TS in the space of under a year.

I don't mean you can not be fast and iterate quickly. I'm as happy as anybody that ES6 and TS are evolving fast. What I mean is that you have to try to predict the future, to avoid putting the language in a dead end.

I could agree on using override keyword. With proper arguments even keeping fields and interfaces out of the scope, but I can not agree with the 'let's stay focused and solve just this particular problem the way other languages do it without thinking too much' argument.

But a number of languages have chosen the override keyword to meet the objectives discussed here so it seems counterproductive to suggest totally alien syntax.

None of this languages have prototypal inheritance or optional method (methods that are neither abstract or virtual, they just could not exist at runtime), and this are related problems that have to be discussed (and maybe discarded) before making a commitment.

Put another way: let's say we do as you seem to suggest and we implement override without thinking to much. Then me, or anybody else using TSX, adds an issue with why override doesn't work with React components. Whats your plan?

kungfusheep commented 8 years ago

In the general case yes, but we're not talking about implementing any interface method. We're talking about an optional interface method where the parent class implements the interface.

It doesn't matter where the interface was set up, the fact is they are not the same thing and so shouldn't share a keyword because the intent of the program is not clear.

You could, for example, be overriding a method which had been implemented for interface compliance in a base class; If we were to put all our eggs in one keyword for these two different things, how would anyone then know if this was the initial declaration of that function or an override to one previously defined in a base class? You wouldn't, and it wouldn't be possible to know without further inspection of the base class - which may even be in a 3rd party .d.ts file & thus making it an absolute nightmare to find, depending how deep in the inheritance chain the function was originally implemented.

Put another way: let's say we do as you seem to suggest and we implement override without thinking to much. Then me, or anybody else using TSX, adds an issue with why override doesn't work with React components. Whats your plan?

Why does this need to fix React? If React has a different problem to what this is trying to solve then I can't for the life of me fathom why override needs to fix it? Have you tried opening another issue to suggest something be done about interface implementation?

I wouldn't agree that enough thought hasn't been put in to this. We're suggesting a tried and tested technique that's been successful in every other language I can think of that's implemented it.

olmobrutall commented 8 years ago

the fact is they are not the same thing and so shouldn't share a keyword because the intent of the program is not clear.

Are not? Look at this two alternative definitions of BaseClass

class BaseClass {
     abstract myMethod(); 
}
interface ISomeInterface {
     myMethod?(); 
}

class BaseClass extends ISomeInterface {
}

And then in your code you do:

class ConcreteClass {
    override myMethod() { 
         // Do stuff
    }
}

You think it should work in just one case and not in the other? The effect is going to be 100% identical in Javascript (creating a new method in ConcreteClass prototype), from the external interface and from the tooling perspective.

Even more, maybe you want to capture this inside of the method, implementing it with a lambda (useful for React event handling). In this case you'll write something like this:

class ConcreteClass {
    override myMethod = () => { 
         // Do stuff
    }
}

The behavior will be again identical if the method is abstract or comes from the interface: add a field in the class with a lambda implementing it. But it looks a little bit strange to override a field, since you're just assigning a value.

Not let's see it using super. (my favorite syntax right now, but I'm open to alternatives).

class ConcreteClass {
    super.myMethod() {  //method in prototype
         // Do stuff
    }

    super.myMethod = () => {  //method in lambda
         // Do stuff
    }
}

Now the concept is conceptually simpler: My super class says that there is a method or field and the ConcreteClass can define it in the prototype / assign it / read it / call it.

Why does this need to fix React?

Is not just react, look at angular:

Of course most of the interfaces are not meant to be implemented, neither all the classes to be overriden, but one thing is clear: in Typescript interfaces are more important than classes.

Have you tried opening another issue to suggest something be done about interface implementation?

How should I call it? override for interface and fields?

We're suggesting a tried and tested technique that's been successful in every other language I can think of.

The languages you have in mind are quite different. They have inheritance based in a static VTable.

In Typescript a class is just an interface + automatic prototype inheritance of methods. And methods are just fields with a function inside.

In order to adapt the override feature to TS this two fundamental differences have to be considered.

Please @kungfusheep make the effort to think about a solution to my problem. If you want to add different keywords and implement them in a second stage is O.K., but take a second to imagine how it should be.

kungfusheep commented 8 years ago

I can't think of another way to say what I've already said. They are not the same. They are similar, but not the same. Please see this comment by one of the TS devs RE: the readonly keyword - https://github.com/Microsoft/TypeScript/pull/6532#issuecomment-179563753 - which strengthens what I'm saying.

olmobrutall commented 8 years ago

I agree with the general idea, but let's see in practice:

class MyComponent extends React.Component<{ prop : number }, { value: string; }> {

    //assign a field defined in the base class without re-defining it (you want type-checking)
    assign state = { value : number}; 

    //optional method defined in an interface implemented by the base class    
    implement componentDidMount(){ 
    }

    //abstract method defined in the base class 
    override render(){  
    }
}

This looks like VB or Cobol, doesn't it?

kungfusheep commented 8 years ago

It looks like it makes sense, at least.

Consider this example, were there only the override (or just one) keyword.

interface IDo {
    do?() : void;
}
class Component implements IDo {
    protected commitState() : void {
        /// do something
    }
    override public do() : void {
        /// base implements 'do' in this case
    }
}

Now lets implement our own component using what we just wrote.

class MyComponent extends Component {
    override protected commitState(){
        /// do our own thing here
        super.commitState();
    }
    override do() : void {
        /// this is ambiguous. Am I implementing this from an interface or overriding a base method? I have no way of knowing. 
    }

}
joewood commented 8 years ago

One way of knowing would be the type of super:

  override do() : void {
        super.do(); // this compiles, if it was an interface then super wouldn't support `do`
    }
kungfusheep commented 8 years ago

exactly! which means the design is wrong. there shouldn't be an investigation involved with skimming code, it should just make sense when you read it.

olmobrutall commented 8 years ago

this is ambiguous. Am I implementing this from an interface or overriding a base method? I have no way of knowing.

What's the difference in practice? The object has just one space of names and you can only place one lambda in the do field. There's no way of explicitly implementing an interface anyway. The implementation is going to be:

MyComponent.prototype.do = function (){
    //your stuff
}

independently of what you write.

kungfusheep commented 8 years ago

It doesn't matter what the output is. You could be either intentionally or unintentionally overriding some functionality in a base class, but there is no intent in a keyword that is ambiguous.

olmobrutall commented 8 years ago

What error or unexpected behavior will be solved by having two keywords?

kungfusheep commented 8 years ago

Come on now mate. You're obviously a clever guy. I've just said "unintentionally overriding some functionality in a base class"; can we not infer from this any unexpected behaviour that could have potentially just occurred?

To be clear, I'm not proposing turning this issue into a proposal for two keywords. This issue is for the override keyword - anything else will need a new proposal and its own discussion on semantics.

olmobrutall commented 8 years ago

To be clear, I'm not proposing turning this issue into a proposal for two keywords. This issue is for the override keyword - anything else will need a new proposal and its own discussion on semantics.

It doesn't really matter in how many issues it should be discussed, or from whom the idea comes. You propose to split two very related thinks and don't even consider a consistent syntax.

The arguments for whether we need 1, 2 or 3 keywords belong to this thread and is not finished (...but becoming repetitive). Then maybe we can discuss the syntax in another thread (because the semantics are going to be identical anyway :P).

In my example:

class MyComponent extends React.Component<{ prop : number }, { value: string; }> {

    //assign a field defined in the base class without re-defining it (you want type-checking)
    assign state = { value : number}; 

    //optional method defined in an interface implemented by the base class    
    implement componentDidMount(){ 
    }

    //abstract method defined in the base class 
    override render(){  
    }
}

Don't assign, implement and override do exactly the same thing: Check that the name exist (in the base class, the implemented interfaces, the interfaces implemented by the base class, etc...).

If there's a name conflict between base classes and some implemented interfaces you'll get an compile-time error whether with 1, 2 or no keyword at all.

olmobrutall commented 8 years ago

Also think about the object literal:

var mc = new MyComponent(); 
mc.state = null;
mc.componentDidMount =null;
mc.render = null;

With exactly the same syntax I can re-assign fields or method independently if they come from the base class, direct interface implementations or interfaces implemented in the base class.

kungfusheep commented 8 years ago

Don't assign, implement and override do exactly the same thing: Check that the name exist (in the base class, the implemented interfaces, the interfaces implemented by the base class, etc...).

You've just described 3 different scenarios there, so obviously they're not the same. I have a feeling I could describe why they're different to you all day and you'd still be sat arguing that they're not, so I'm going to bow out of this particular line of discussion for now. There's nothing to say the TS guys are still considering this at the moment anyway.

RyanCavanaugh commented 8 years ago

With the closure of #6118 I think there's reason to discuss if the problems there and the problems here can be addressed simultaneously.

olmobrutall commented 8 years ago

I was not aware of https://github.com/Microsoft/TypeScript/pull/6118. The idea looks like a possible alternative to adding override.

If I understood the problem properly, the issue is that you can have more than one base classes / interfaces with compatible but not identical member declarations, and they have to be unified when being initialized in the child class without a type.

Without having a good idea of the consequences, I would be happy if:

More important IMO would be to have some way of triggering auto-completion when writing class members (Ctrl+Space). When the cursor is directly inside of a class, you could be defining new members of re-defining inherited ones, so the auto-completion can not be too aggressive, but with manual triggering the behavior should be OK.

eggers commented 8 years ago

Regarding @RyanCavanaugh comments:

We definitely understand the use cases here. The problem is that adding it at this stage in the language adds more confusion than it removes. A class with 5 methods, of which 3 are marked override, wouldn't imply that the other 2 aren't overrides. To justify its existence, the modifier would really need to divide the world more cleanly than that.

Typing a variable as any doesn't imply that some other variable is or isn't also any. But, there is a compiler flat --no-implicit-any to enforce that we explicitly declare it. We could equally have a --no-implicit-override, which I for one would turn on if it were available.

Having an override keyword that is used gives developers an enormous amount of insight when reading code that they are unfamiliar with, and the ability to enforce it would give some additional compile time control.

For all the +1ers -- can you talk about what's not sufficient about the decorator solution shown above?

Is there any way in which a decorator a better solution than an override keyword? There are many reasons why it is worse: 1) It adds runtime overhead however small; 2) It's not a compile time check; 3) I have to include this code to every one of my libraries; 4) There is not way for this to catch functions that are missing the override keyword.

Let's give an example. I have a library with three classes ChildA, ChildB, Base. I've added some method doSomething() to ChildA and ChildB. After some refactoring, I've added some additional logic, optimized, and moved doSomething() to the Base class. Meanwhile, I have another project which depends on my library. There I have ChildC with doSomething(). There is no way when I update my dependencies to find out that ChildC is now implicitly overriding doSomething(), but in an unoptimized way that is also missing some checks. That is why an @overrides decorator will never be sufficient.

What we need is an override keyword, and a --no-implicit-override compiler flag.

JabX commented 8 years ago

An override keyword would help me a lot since I use in my project a simple hierarchy of base classes to create all my components. My problem lies in the fact that these components may need to declare a method to be used somewhere else, and this method may or may not be defined in a parent class and may or may not already do stuff that I need.

For example, let's say that a function validate takes a class with a getValue() method as parameter. To build this class, I can inherit another class that could already define this getValue() method, but I can't really know this unless I look at its source code. What I'd instinctively do is implement this method in my own class, and as long as the signature is correct no one will tell me anything.

But maybe I shouldn't have done that. The following possibilities all suppose that I did an implicit override:

  1. The base class already defined this method just like I did, so I wrote a function for nothing. Not that much of a problem.
  2. I incorrectly rewrote the function, most of the time because I forgot to do some non-obvious stuff that was already handled by the base class. What I really needed to do here is call super in my override, but no one suggested me to do so.

Having a mandatory override keyword would have told me "hey, you're overriding, so maybe you should check what the original method looks like before doing your stuff". That would greatly improve my experience with inheritance.

Of course, as suggested, it should be placed under a --no-implicit-override flag since it would be a breaking change and that most people do not care that much about all this.

I like the comparaison @eggers made with any and --no-implicit-any because it's the same kind of annotation and it would work in the exact same way.

eggers commented 8 years ago

@olmobrutall I was looking at some of your talk about overriding abstract methods & interface methods.

If my opinion, override implies the existence of a super. Neither abstract methods nor methods defined in an interface can be called via a super. call, and so should not be overrideable (there is nothing to override). Instead if we do something more explicit for those cases, it should be an implements keyword. But, that would be a separate feature discussion.

RyanCavanaugh commented 8 years ago

Here are the problems as I see them, ranked most important to least. Did I miss anything?

Failure to override

It's easy to think you're overriding a base class method when you're not:

class Base {
  hasFilename(f: string) { return true; }
}
class Derived extends Base {
  // oops
  hasFileName(f: string) { return false; }
}

This is probably the biggest problem.

Failure to implement

This is very closely related, especially when the implemented interface has optional properties:

interface NeatMethods {
  hasFilename?(f: string): boolean;
}
class Mine implements NeatMethods {
  // oops
  hasFileName(f: string) { return false; }
}

This is a less important problem than failure to override, but it's still bad.

Accidental override

It's possible to override a base class method without realizing it

class Base {
  hasFilename(f: string) { return true; }
}
class Derived extends Base {
  // I didn't know there was a base method with this name, so oops?
  hasFilename(f: string) { return true; }
}

This should be relatively rare just because the space of possible method names is very large, and the odds that you'd also write a compatible signature and not mean to have been doing this in the first place are low.

Accidental anys

It's easy to think that override methods will get the parameter types of their base:

class Base {
  hasFilename(f: string) { return true; }
}
class Derived extends Base {
  // oops
  hasFilename(f) { return f.lentgh > 0; }
}

We tried to type these parameters automatically but ran into problems. If hasFilename were explicitly marked override we might be able to solve those problems more easily.

Readability

It's not immediately clear when a method is overriding a base class method and when it's not. This seems like a smaller problem because there are reasonable workarounds available today (comments, decorators).

Editor experience

You have to manually type out base method names when writing derived class override methods, which is annoying. We could easily fix this by always providing those names in the completion list (I think we actually have a bug on this already), so we don't really need a language feature to fix this problem.

Non-problems

Listing these out as things that either belong in a separate issue or simply aren't going to happen:

eggers commented 8 years ago

@RyanCavanaugh I agree with all of the above in about that order as well. Just as a comment, I don't think that the override keyword should be solving the "failure to implement" problem. As I said above, I think that problem should be solved by a different keyword (e.g. implements) as part of a different ticket. (override should imply a super. method)

olmobrutall commented 8 years ago

@RyanCavanaugh I agree with all the points but I don't see any reference to the also very related problem of initialized fields declared in a parent type without overriding the type (and then loosing type checking). I think the feature should not be confined to method declarations in a language where methods are just functions in object fields.

@eggers even if at the end the end two keywords are needed, the semantics will be very similar and I think the features should be discussed together.

override should imply a super. method

In C# override can (and must) be used also for abstract methods (with no .super). In Java @override is an attribute. Of course they are different language but with similar keywords you expect similar behaviors.

JabX commented 8 years ago

I agree with @RyanCavanaugh 's points, though I'd say the "accidental override" problem might be a little more common than he believes (especially when dealing with extending a class that already extends something else, like a React component that would already define a componentWillMount method in the first extension).

I believe though, like @eggers said, that the "failure to implement" issue is something pretty different, and it would feel weird to use an override keyword for a method that doesn't exist in a parent class. I know these are different languages with different problematics, but in C# override isn't used for interfaces. I would suggest, if we could get a --no-implicit-override flag, that interface methods would have to be prefixed with the interface name (which would look a lot like C# and thus feel more natural).

Something like

interface IBase {
    method1?(): void
}

class Base {
    method2() { return true; }
}

class Test extends Base implements IBase {
    IBase.method1() { }
    override method2() { return true; }
}
kevmeister68 commented 8 years ago

I think @RyanCavanaugh lists the fundamental problems. I would complement that with "What are the disagreements":

    interface IDrawable
    {
        draw?( centerPoint: Point ): void;
    }

    class Square implements IDrawable
    {
        draw( centerPoint: Point ): void; // is this an override?
    }
    interface IPoint2
    {
        x?: number;
        y?: number;
    }

    class Circle implements IPoint2
    {
        x: number; // is this an override?
        y: number; // is this an override?
        radius: number;
    }
olmobrutall commented 8 years ago

@kevmeister68 thanks for explicitly stating the disagreements.

One thing: you have to make the interface members optional to really show the problem, like this the typescript compiler will complain when a field or method is not implemented, solving "Failure to implement".

kevmeister68 commented 8 years ago

@olmobrutall Thanks for that. I've never declared a method optional - can it be (I come from a C# background and there is no such concept)? I've updated the examples I listed above to change the member variables to optional - is that better?

olmobrutall commented 8 years ago

@kevmeister68 also the draw method in IDrawable :)

olmobrutall commented 8 years ago

@RyanCavanaugh

Insufficiently exotic syntax (e.g. implements.foo = ...). No need to bikeshed here

Thanks for teaching me a new expression. I don't see a lot of problems in the semantics of the feature:

Most of the problems rely on the syntax and the behaviours that they impliy:

Let's compare the tree more promising syntaxes:

override / implements

class Person{
    dateOfBirth: Date;

    abstract talk();
    walk(){ //...}
}

interface ICanFly{
    fly?();
    altitude?: number;
}

class SuperMan extends Person implements ICanFly {
     dateOfBirth = new Date(); //what goes here?

     override talk(){/*...*/}
     walk = () => {/* force 'this' to be captured*/}  //what goes here

     implements fly() {/*...*/}
     altitude = 1000; //what goes here?
}

Advantages:

Disadvantages:

override / InterfaceName.

class Person{
    dateOfBirth: Date;

    abstract talk();
    walk(){ //...}
}

interface ICanFly{
    fly?();
    altitude?: number;
}

class SuperMan extends Person implements ICanFly {
     dateOfBirth = new Date(); //what goes here?

     override talk(){/*...*/}
     walk = () => {/* force 'this' to be captured*/}  //what goes here

     ICanFly.fly() {/*...*/}
     ICanFly.altitude = 1000; //what goes here?
}

Advantages:

Disadvantages:

this.

class Person{
    dateOfBirth: Date;

    abstract talk();
    walk(){ //...}
}

interface ICanFly{
    fly?();
    altitude?: number;
}

class SuperMan extends Person implements ICanFly {
     this.dateOfBirth = new Date();

     this.talk(){/*...*/}
     this.walk = () => {/* force 'this' to be captured*/} 

     this.fly() {/*...*/}
     this.altitude = 1000;
}

Advantages:

Disadvantages:

kungfusheep commented 8 years ago

Whilst failure to implement a non-optional interface method will cause a compiler error, if the interface changes at some point in the future (lets say a method is removed) there will be no warning for all classes that implemented that method. This might not be as big an issue as optional's but I think it's fair to say the scope of this goes beyond them.

I do however, still believe override is a distinctly different thing and potentially having two keywords rather than one would better convey the intent of the program.

For example, consider a scenario where a developer believes they are implementing an interface method when in-fact they're overriding the method which has already been declared in a base class. With two keywords the compiler can prevent this kind of mistake and throw an error to the tone of method already implemented in a base class.

interface IDelegate {
    execute?() : void;
}

class Base implements IDelegate {
    implement public execute() : void { /// fine, this is correctly implementing execute
    }
}

class Derived extends Base {
    implement public execute() : void { 
/// ERROR: `method "execute():void" already implemented in a base class`
    }
}
JabX commented 8 years ago

I don't know if this is relevant in JS, but class fields are supposed to be private in OO, so I don't think we need to be overriding fields explicitly since the fact that they're private already prevent us to override altogether.

Instance methods are fields though, and from what I gather a lot of people use them instead of prototype methods. About that,

class Person{
    walk(){ //...}
}
class SuperMan extends Person  {
     walk = () => {/* force 'this' to be captured*/}
}

is a compiler error, because walk is a prototype method in Person and an instance method in SuperMan.

Anyway, i'm not sure override would be a fit here, because well, we don't override fields. Again, it would look like C#, but I'd rather use the new keyword instead of override here. Because it's not the same thing as a method override (I can call super.myMethod in an override, not here).

My preferred solution would then be something like (assuming we're in strict override mode):

class Person{
    dateOfBirth: Date;
    talk() { }
    walk = () => { }
}

interface ICanFly {
    fly?();
    altitude?: number;
}

class SuperMan extends Person implements ICanFly {
     new dateOfBirth = new Date();
     override talk() { }
     new walk = () => { }

     implements fly() {/*...*/}
     implements altitude = 1000;
}

My main concern is about interfaces though. We shouldn't have to write implements for non optional interface members, because we'll be already caught by the compiler. Then, if we do so, there'll be a confusion between what is from an interface and what isn't, since not all interfaces members would be prefixed by implements. And that word is a mouthful. I'm not ready to write something like this:

class C extends React.Component {
    implements componentWillMount() { }
    implements componentDidMount() { }
    implements componentWillReceiveProps(props) { }
    /// ... and the list goes on
}

I proposed to prefix with the interface name earlier but @olmobrutall showed me that it was a worse idea.

Anyway, I'm pretty convinced that we'll need 3 different new keywords to properly tackle this.

I also don't think that it's necessary to implicitly type the overrides since the compiler will already prevent us to write something that's not compatible, especially because it is apparently difficult to do right.

olmobrutall commented 8 years ago

Isn't new, override and implement too much keyword overhead for essentially the same JS semantics? Even if in C# they are different things there is virtual no difference in JS/TS.

It's also quite ambiguous:

Also think about this:

class Animal {
}

class Human extends Animal {
}

class Habitat {
    owner: Animal;
}

class House extends Habitat {
    owner = new Human();
}

var house = new House();
house.owner = new Dog(); //Should this be allowed??  

The question is:

Does owner = new Human(); re-define the field with a new (but compatible) type, or just assigns a value.

I think in general you re-defined the field except if you using the magic keyword. My preference is for this. right now.

class House extends Habitat {
    this.owner = new Human(); //just setting a value, the type is still Animal
}
JabX commented 8 years ago

@olmobrutall That might indeed be a bit much keyword overhead, but all 3 are indeed different things.

If you're changing a method from a base class that was implemented with an interface what should you use? I can see four possibilities: override , implements , any of the two or both at the same time?

It seems pretty logical to me that it would be override since that what you're effectively doing here. implements would mean that you're implementing something from the interface that doesn't exist otherwise. In a sense, override and new would have priority over implements.

And about your field override exemple, nothing should change about how it works today. I'm not sure what happens here, but whatever it is it shouldn't change (or maybe it should, but that's not what we're discussing here).

malibuzios commented 8 years ago

I feel that override would be suitable for both base methods and properties, including abstract ones (explained below).

new is an interesting idea in concept, but this particular keyword may be confused with class instantiation, so it may give the false impression it would only work for classes, despite the fact that properties can have primitive, interface, union or even function types. Perhaps a different keyword like reassign could work better there, but it has the problem that it may give a false idea in the case the value is only re-declared but not actually assigned with anything in the derived class (new may have that problem as well). I think redefine is also interesting, but could lead to the mistaken expectation that the 'redefined' property could also have a different type than the base one, so I'm not sure.. (edit: actually I checked and it could have a different type as long as the new one is a subtype of the base one, so this may not be that bad).

implement (I prefer this particular verb form for consistency with override) seems to work for interfaces. I believe it could also technically work for abstract base methods as well, but using override could feel more consistent, despite the slightly different semantics. Another reason is that it would make it a bit inconvenient to change a method from abstract to non-abstract, as one would need and go to all the derived classes and change override to implement.

There could be better ideas, but that's all I have at this point..

JabX commented 8 years ago

I proposed the keyword new because it's the one C# uses for this exact feature, but I agree this isn't the best one. implement is indeed a better choice over implements. I don't think we should use it for abstract methods since they're part of a base class and not an interface. override + new -> base class, implement -> interface. That seems clearer.

olmobrutall commented 8 years ago

@JabX

About overriding prototype with instance field

I've checked and you're right:

class Person{
    walk(){ //...}
}
class SuperMan extends Person  {
     walk = () => {/* force 'this' to be captured*/}
}

Fails with Compiler error: Class 'Person' defines instance member function 'walk', but extended class 'SuperMan' defines it as instance member property.

I don't really see the reason to fail in this case, since the parent type contract is fulfilled and you can write this anyway:

var p = new SuperMan();
p.walk = () => { };

Or even

class SuperMan extends Person {
    constructor() {
        super();
        this.walk = () => { };
    }
}

About override

override would apply to methods (defined on the prototype), meaning that it does not erase the original method, which is still accessible behind super.

That's actually an argument. There is least some observable difference between override and implements... but not quite.

Both override and implements are implemented in the prototype, being able to use super is independent, because you call super.walk() not just super(), and is available in every method ( overrides, implements, news and normal definitions).

class SuperMan extends Person implements ICanFly {
     new dateOfBirth = new Date();
     override talk() { } //goes to prototype
     new walk = () => { }

     implements fly() {/*...*/}  //also goes to the prototype
     implements altitude = 1000;
}

And being able to use super.walk() also works if you assign to the instance

class SuperMan extends Person {
    constructor() {
        super();
        this.walk = () => { super.walk(); };
    }

    //or with the this. syntax
    this.walk = () => { super.walk(); };
}

There's already a clear way to differentiate prototype fields from instance fields and is the = token.

class SuperMan extends Person implements ICanFly {
     this.dateOfBirth = new Date(); //instance
     this.talk() { } //prototype
     this.walk = () => { } //instance

     this.fly() {/*...*/}  //prototype
     this.altitude = 1000; //instance
}

With the this. syntax the problem is solved more elegantly:

About New

new would apply to fields, and mean that the original field has been erased by the new one.

Take into account that you can not change the field or method with a different type because you will break the type system. In C# or Java when you do new the new method will be used only on statically dispatched calls to the new type. In Javascript all the calls will be dynamic and if you change the type the code will likely break at run-time (Coercing the type could be allowed for practical purposes however, but not widening).

class Person {
    walk() { }

    run() {
        this.walk();
        this.walk();
        this.walk();
    }
}

class SuperMan extends Person {
    new walk(destination: string) { } //Even if you write `new` this code will break
}

So more than new the keyword should be assign, because this is the only type-safe thing you can do:

class Person{
    dateOfBirth: Date;
}

class SuperMan extends Person implements ICanFly {
     assign dateOfBirth = new Date();
}

Note that reassign doesn't make sense, because Person only declares the field but doesn't set any value on it.

Writing assign seems redundant however, it's clear that we're assigning because we have a = at the other side.

Again the this. syntax solves the problem gracefully:

class Person{
    dateOfBirth: Date;
}

class SuperMan extends Person implements ICanFly {
     this.dateOfBirth = new Date();
}