microsoft / TypeScript

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

Support final classes (non-subclassable) #8306

Closed Zorgatone closed 8 years ago

Zorgatone commented 8 years ago

I was thinking it could be useful to have a way to specify that a class should not be subclassed, so that the compiler would warn the user on compilation if it sees another class extending the original one.

On Java a class marked with final cannot be extended, so with the same keyword on TypeScript it would look like this:

final class foo {
    constructor() {
    }
}

class bar extends foo { // Error: foo is final and cannot be extended
    constructor() {
        super();
    }
}
MrMatthewLayton commented 6 years ago

@bcherny @mhegazy I completely agree with the above. I cannot see any well argued reason why final can't just be another compile-time constraint, like most of the syntactic sugar we see in TypeScript - let's face it, static typing, generics, access modifiers, type aliases, interfaces...ALL SUGAR! I get that the TypeScript team probably want to focus on taking the language in other directions, but seriously, this is OOP 101... this should have been though about a long time ago, when TypeScript was still very young!

MrMatthewLayton commented 6 years ago

@mhegazy may I refer you back to an early comment you made on this issue?

a class with private constructor is not extendable. consider using this instead.

At the time of writing this comment has been down-voted 21 times. Clearly this is NOT WHAT THE COMMUNITY WANT AS A SOLUTION!

Zorgatone commented 6 years ago

wow, got a lot of feedback about this. I can't understand why no interest to implement

RyanCavanaugh commented 6 years ago

Lots of really unproductive complaining going on here. Please don't just show up to voice frustration - we are allowed to make decisions here and the community is allowed to give their feedback, but just idle gnashing of teeth is not going to change anyone's mind. Please be specific and actionable; we're not going to be convinced by people just showing up to say DO THIS FEATURE NOW.

Note that this issue is about final/sealed classes. There's been off-topic discussion of final/sealed methods which are a completely different concept.

Some points were considered when we reviewed this last time

You are free to disagree with all of these, but please bring some actionable and specific feedback about how the lack of final is harming your ability to effectively use JavaScript classes.

RyanCavanaugh commented 6 years ago

haven't heard a convincing argument for why TS doesn't need it

Just a reminder that this is not how it works. All features start at -100. From that post

In some of those questions, the questions asks why we either “took out” or “left out” a specific feature. That wording implies that we started with an existing language (C++ and Java are the popular choices here), and then started removing features until we got to a point where we liked. And, though it may be hard for some to believe, that's not how the language got designed.

We have a finite complexity budget. Everything we do makes every next thing we do 0.1% slower. When you request a feature, you're requesting that every other feature become a little bit harder, a little bit slower, a little bit less possible. Nothing gets in the door here because Java has it or because C# has it or because it'd only be a few lines of code or you can add a commandline switch. Features need to pay for themselves and for their entire burden on the future.

bcherny commented 6 years ago

@RyanCavanaugh While I'm not sure I agree with the decision, I do appreciate the really thoughtful and detailed response, and for your taking the time to write it. I get that you want to keep the language lean - it's good to reflect on whether features that other popular languages have are really valuable, or just common.

alpapad commented 6 years ago

@RyanCavanaugh

I don't think the point here, in this forum is .. "effectively use JavaScript classes."

Sorry, I can not resist, and just add to the "unproductive complaining going on here": the final keyword is usefull for all the reasons explained, in detail, and with arguments by others.

hk0i commented 6 years ago

We are trying our best to avoid turning TypeScript into an OOP keyword soup. There are many better composition mechanics than inheritance in JavaScript...

Like when you use final to enforce composition over inheritance in (insert not typescript language name here) 😈 (Sorry, I couldn't resist)

But more to the point, it seems like TS intends to mostly be a compatibility layer to introduce "today's JavaScript now" or something to that effect (a la Babel) with the exception that it adds some additional type checking.

I think the gist of the counter argument for implementing this is that if you want another language's features, use the other language instead. If you want JavaScript, use JavaScript. If you want TypeScript, use that.

I know Kotlin can compile to JS and has a lot of the features that I think the people here would enjoy so that is something that might be useful to look into. I think it does add some JS library as a dependency though (I haven't tried it myself).

The main takeaway being that if you're not happy with TypeScript, don't use it.

MrMatthewLayton commented 6 years ago

@RyanCavanaugh

On the points you raised above...

CLASSES ARE A JAVASCRIPT FEATURE, NOT A TYPESCRIPT FEATURE. If you really feel like classes are completely useless without final, that is something to take up with the TC39 committee or bring to es-discuss. If no one is willing or able to convince TC39 that final is a must-have feature, then we can consider adding it to TypeScript.

I've been following TypeScript since 0.7 and back then, IIRC it was targeting ES3-ES5. Classes were definitely a TypeScript feature back then. The same argument applies to decorators - they're on the cards for ES, but they're already here in TypeScript as an experimental feature.

As a community, we're not arguing that the TypeScript compiler must somehow emit a JavaScript class that's final . A compile-time check would suffice, in the same way that compile time checks for access modifiers suffice; after all, they're not JavaScript features either, but TypeScript still makes them possible.

If final did become an ES feature, then presumably you could refactor that bit of the emitter when targeting ES* to output final appropriately (In the same what you did for classes, when they were introduced in ES6)?

If it's legal to invoke new on something, that has a one-to-one runtime correspondence with inheriting from it. The notion of something that can be new'd but not super'd just doesn't exist in JavaScript

Again, I don't think the community are expecting you to magic something up here to enforce final at runtime. As you've accurately stated, it "doesn't exist in JavaScript", but as mentioned, a compile-time constraint would suffice.

We are trying our best to avoid turning TypeScript into an OOP keyword soup. There are many better composition mechanics than inheritance in JavaScript and we don't need to have every single keyword that C# and Java have.

It's a fairly well known phrase in the software development community as a whole: "Favour composition over inheritance", however "favour" doesn't mean don't use inheritance as a blanket statement. Sure, you don't need every single keyword that C# and Java have, but you must have had a rationale for allowing abstract classes (which by the way "doesn't exist in JavaScript"), so why not final classes?

You can trivially write a runtime check to detect inheritance from a "should-be-final" class, which you really should do anyway if you're "worried" about someone accidently inheriting from your class since not all your consumers might be using TypeScript.

The same could be said for C# and Java, but I don't need runtime checks because I have sealed and final to do that for me at the compiler level.

If not all my consumers were using TypeScript, I'd have a lot more to worry about than just inheriting from a final class; for example, access to private or protected members, no static type checking, or the ability to construct an abstract class.

Should our code really be littered with runtime checks for all these things (some of which aren't even possible I might add) just to make sure our non-TypeScript consumers are as safe as possible?

You can write a lint rule to enforce a stylistic convention that certain classes aren't inherited from

This would imply that whoever is consuming your code is running the linter, so it's still not really a proper solution; it's a workaround.

The scenario that someone would "accidently" inherit from your should-be-sealed class is sort of an odd one. I'd also argue that someone's individual assessment of whether or not it's "possible" to inherit from a given class is probably rather suspect.

Good/Great developers think about their rationale for doing something, like extending a class. Any less than that and you end up with a "but it works" mentality, without really understanding why.

Asking a question that starts with can I usually tends to have a more definitive answer than questions that start with should I.

There are basically three reasons to seal a class: Security, performance, and intent. Two of those don't make sense in TypeScript, so we have to consider the complexity vs gain of something that's only doing 1/3rd the job it does in other runtimes.

Arguably I'd say that TypeScript would actually fulfil two of these reasons: Security and intent, however if this was implemented purely as a compile time check, then it could only do this at the compiler level, rather than, as you rightly point out, at the runtime level.

I don't think that not having final is harming my ability to effectively use JavaScript classes, nor are JavaScript classes unusable without it. I feel that final is more on the side of a "nice to have" feature, rather than a "necessary" feature, but then the same could be said for many of TypeScript's features.

I think the main gripe here is that TypeScript allows us to create abstract and open classes, but in terms of inheritance and polymorphism it doesn't allow us to paint a complete picture in an elegant and expressive way.

Hiding the constructor isn't what the community want because this takes semantic and expressive value away from class implementations, and once again, because access modifiers are a "nice to have", this can't even be enforced at runtime; essentially a "doesn't exist in JavaScript" situation.

As a developer I have many hats; at the moment I have a C# hat, a Kotlin hat and a TypeScript hat.

Out of interest, when the TypeScript team sat around a table and discussed abstract classes, did anyone raise their hand and say "but what about final"?

niokasgami commented 6 years ago

@RyanCavanaugh Hum I do understand your points

I think mostly it's was an misunderstanding (I guess from how we explained)

@series0ne summary really well that our intentions wasn't too complicate the typescript output by asking to create final class in JS (heck this would be an train wreck to do so) but to just create extra rules for tell peoples : Nope you can't do that. and it's tell the user of the API that they probably shouldn't mess with those class (for stability, security reason etc) ALL on the typescript files/compiler NOT on the output.

Do I think it's would be nice to have this feature : yes it's nice to have more fast way to do some technique.

Is it TRULY needed? Not really this could be something considerate minor and shouldn't be a priority if the Typescript team can afford to implement this extra then it could be nice to have.

I do see that the final/sealed keyword could complicate a LOTS some aspect especially : Bug fix, Patch, change of behavior.

if I explain this, I could totally see an abusive use of final by locking ALL the class from being extended. Provoking IMO a big pain in the a** to deal with. I don't want to use an API where I am locked to do any change to the user class by inheritance because they was to paranoid because of "security".

This would mean I would have to go using prototype then alias or override this class just for the sake to be able to change this class behavior for fit my needs. this would be annoying especially if I just want to add extra functionality to the said class and don't want to go with a destructive workflow.

After this just my though I might misunderstand myself the situations right now so be free to explain it to me :)

niokasgami commented 6 years ago

Ah if I recall I was reading a lots about the ES4 (I am unsure I was still a kid when the draft was in productions lol) and surprisely it's look really similar to Typescript.

and it's say in the draft specifications here : https://www.ecma-international.org/activities/Languages/Language%20overview.pdf

A class that does not explicitly extend any other class is said to extend the class Object. As a consequence, all the classes that exist form an inheritance hierarchy that is a tree, the root of which is Object. A class designated as final cannot be extended, and final methods cannot be overridden: class C { final function f(n) { return n*2 } } final class D extends C { function g() { return 37 } }

so Technically the final keyword was planned into the Ecmascript 4th edition. Do we will see it in the futur integrations of JS I doubt about it but they indeed integrated class so this might be possible but who know?

leviathanbadger commented 6 years ago

I have one use case that I just barely ran into that drove me to find this issue. It revolves around the use of this as a return type:

abstract class TileEntity {
    //constructor, other methods...
    abstract cloneAt(x: number, y: number): this;
}

My intention in writing this was to ensure that TileEntities are immutable (and therefore safe to pass around) but that any given TileEntity could be cloned with a few changed properties (in this case x and y) without having to know anything about the shape of the subclass. That is what I want to write, but attempting to write an implementation (correctly) causes this to break.

class TurretTileEntity extends TileEntity {
    //constructor, other methods...
    cloneAt(x: number, y: number): this {
        return new TurretTileEntity(x, y, /* ... other properties */);
    }
}

The problem is that without being able to declare that there will never be a subclass of TurretTileEntity, the this return type cannot be narrowed down to just the type TurretTileEntity.

The impact of this shortcoming in Typescript is minimal, considering you can cast the return value to <any>. This is, objectively, an antipattern, and a useful indication that the type system could be more expressive. Final/sealed classes should be supported.

RyanCavanaugh commented 6 years ago

Out of interest, when the TypeScript team sat around a table and discussed abstract classes, did anyone raise their hand and say "but what about final"?

Yes. Every time we add a keyword, "but then we'll have to add this other keyword too!" is a strong point against adding it, because we want to grow the language as carefully as possible. final on class declarations is in this camp as well - if we have final classes, then we'll "need" final methods or properties as well. Anything that looks like scope creep is regarded with extreme suspicion.

harshudhwani commented 6 years ago

I think that final classes and methods are very important. I hope you will implement it.

ghost commented 6 years ago

Note that this issue is about final/sealed classes. There's been off-topic discussion of final/sealed methods which are a completely different concept.

mhegazy closed https://github.com/Microsoft/TypeScript/issues/9264 in favor of this issue. Is there a separate issue that is not closed as a dupe to track final/sealed methods?

MrMatthewLayton commented 6 years ago

@crcla From reading some of @RyanCavanaugh's comments above, I believe the rationale for closing the other issue is because supporting final/sealed classes encompasses supporting final/sealed methods. The two are quite closely related topics.

huan commented 6 years ago

I can not believe the propose for final is labeled as Won't fix.

Change the label back to In Discussion and implement it, please!

In my case, I want to write an abstract base class for the plugins and I want to make sure that the plugin class can not overwrite my base class methods by mistake. None of the private constructor, Object.seal can do this job.

pelotom commented 6 years ago

To those who see this as a crucial, live-or-die feature: it's really not. If your API consists of exposing a class implementation that needs to be extended by the consumer, there are better ways to do this. Don't follow React's bad example. Just use functions and simple objects, and leave this OO nonsense where it belongs: in the past.

Now bring on the downvotes 😀

thorek commented 6 years ago

Could someone explain me how this discussion does not distinct between final classes and final methods? Of course preventing some methods from overriding adds enormous value. I want to provide the possibility for a plugin developer to add functionality by subclassing some of my frameworks classes and implementing abstract functions - but at the same time make sure she does not (why ever!) override the methods that call these abstract functions (and make sure the error handling/loggin/checking takes place.

kerberjg commented 6 years ago

@pelotom If you don't want to use OOP constructs and patterns, just don't, no one's telling you to do otherwise, however the same should apply in the opposite direction: if someone wants to use OOP constructs, just let them. By forcing others to adopt a specific (non-OOP) pattern, you're adopting the same behaviour as the one you denounce OOP for. At least be consistent :)

Adding a final keyword to TypeScript is not a breaking change and won't influence any of your existing projects in any way, however it will be a vital feature for people such as @thorek, myself, and many others.

pelotom commented 6 years ago

if someone wants to use OOP constructs, just let them.

We’re not debating whether people should use OOP constructs, we’re debating whether currently non-existent OOP constructs should be added to the language. Doing so takes developer time that could be spent on other features, and makes the language more complex, which slows the rate of future features (because for every new feature it must be considered how it will interact with every old feature). So it certainly does affect me, because I’d rather spend TypeScript’s development and complexity budget on more useful things!

iteriani commented 6 years ago

fwiw, we're probably going to encourage our customers (google) to use @final in comments so it propagates to closure compiler :\

dimiVergos commented 6 years ago

Adding final on classes and methods is useful when an R&D team builds TS libraries that other teams can use. These keywords are (an important) part of service exposure stability of such tools. The keyword is more important for large teams and less important for small projects, imho.

kerberjg commented 6 years ago

@dimiVergos This is exactly my case, and I assume the same for many others. I can imagine I may seem biased due to my position, but it seems the only (albeit highly) justifiable use of this keyword according to my experience.

I'm open to corrections on this one!

cbutterfield commented 6 years ago

I care more about supporting final methods (to avoid subclasses accidentally overriding them) than I do final classes. Is the the right ticket to vote for that? Ticket https://github.com/Microsoft/TypeScript/issues/9264 seems to imply so.

So how do I vote for "final methods"? Or did I just vote for it by this comment? Also how do we add "methods" to the title? Can I just edit it? The current title is misleadingly limited solely to classes but the discussion includes methods too.

ghost commented 6 years ago

Like many others I would like to see final methods in TypeScript. I've used the following approach as a partial workaround that is not immune to failure but at least helps a little:

class parent {
  public get finalMethod(): () => void {
    return () => {
      // final logic
    };
  }
}

class child extends parent {
  // ERROR "Class 'parent' defines instance member accessor 'finalMethod', but extended class 'child' defines it as instance member function"
  public finalMethod(): void { }
}
ChuckJonas commented 6 years ago

Here's another real world use case for final methods. Consider the following extension of a React.Component to make it easier to implement a simple Context.


interface FooStore{
  foo: string;
}

const {Provider, Consumer} = React.createContext<FooStore>({
  foo: 'bar';
});

abstract class FooConsumerComponent<P, S> extends React.Component<P, S> {

  // implement this instead of render
  public abstract renderWithStore(store: FooStore): JSX.Element;

  public final render() {
    return (
      <Consumer>
        {
          (store) => {
            return this.renderWithStore(store);
          }
        }
      </Consumer>
    );
  }
}

Without final on the render() method, nothing stops one from incorrectly overriding the method, thus breaking everything.

yd021976 commented 6 years ago

Hi, like many others I would like "final" or "sealed" keyword at least for methods. I totally agree that this keyword is a really added value as it allow developper to prevent their librairies customer to extend crucial code blocks (like in plugin for example).

The only workaround I found is to use method decorator like : function sealed(target, key,descriptor){ descriptor.writable = false; // Prevent method to be overriden }

then in your "base" class, use decorator to prevent method override, example :

class MyBaseClass {
@sealed
public MySealedMethod(){}

public OtherMethod(){}

...
}

This way 'MySealedMethod' couldn't (easily) be overriden in sub class. Exemple:

class AnotherClass extends MyBaseClass {
public MySealedMethod(){} ====>> ERROR at RUNTIME (not at compile time)
}

actually, the only downside of this workaround is that the "sealed" is only visible at runtime (error in javascript console) but NOT at compile time (as the method properties are set via function I guess)

asimonf commented 5 years ago

@RyanCavanaugh

Yes. Every time we add a keyword, "but then we'll have to add this other keyword too!" is a strong point against adding it, because we want to grow the language as carefully as possible.

It seems to me that you're starting this argument with your mind set up against the feature. A popular feature should not be a point against considering such feature. It should be a point towards asking yourself why it's popular, and then carefully consider it.

I'm sure you've discussed it and arguments have been flung around towards considering it or against it. Is the argument against it just that it's feature creep, or is there anything more specific? Do you consider it a fringe feature? If so, could we do something to change your mind?

Let me just leave these questions here for all of us to ponder.

Is there a way to seal a class by design using present constructs? If the feature was added, would it allow us to do anything new? (As in, something present constructs don't permit) If the feature did allow us to do something new, is it something valuable? Would adding sealed classes increase language complexity too much? Would adding sealed classes increase compiler complexity too much?

For the purpose of discussing these, I'm not including sealed methods in the discussion and I'm also not including run-time checks in the implementation. We all understand we can't enforce this on Javascript.

FYI: I'd be awesome if the counter to this feature was a bit more than just blanket statements against feature-creep.

RyanCavanaugh commented 5 years ago

@asimonf I think everything you've said was already addressed by this comment (you will need to expand the "show more" section to see it) and the one following it.

What would change our mind here? People showing up with examples of code that didn't work the way it should have because the sealed keyword was missing.

The scenario here that the keyword attempts to address is one where someone mistakenly inherits from a class. How do you mistakenly inherit from a class? It's not an easy mistake to make. JavaScript classes are inherently fragile enough that any subclass must understand its base class's behavioral requirements, which does mean writing some form of documentation, and the first line of that documentation can simply be "Do not subclass this class". Or there can be a runtime check. Or the class constructor can be hidden behind a factory method. Or any other number of options.

Why must TypeScript have this keyword when the situation is already one in which you should have had encountered at least two separate roadblocks telling you not to be there to begin with?

I've been programming for approximately forever years now and I have not once tried to subclass something only to find out it was sealed and I shouldn't have tried. It's not because I'm a super genius! It's just an extremely uncommon mistake to make, and in JavaScript these situations are already ones where you need to be asking questions of the base class in the first place. So what problem is being solved?

Xample commented 5 years ago

A real case I would be using final is to prevent overriding a method with something which would never call the super method. Some public method of course. But the real solution I figured out would be the ability to force any sub method to call the super one. https://github.com/Microsoft/TypeScript/issues/21388

dudewad commented 5 years ago

As I'm not a language designer, I can't speak to the drawbacks of adding a keyword to the language. I will say that I am currently working on an abstract superclass that has a method I intend to be final, but to my dismay found that I couldn't make it so. For now I will go with the less-than-ideal approach of putting a comment, but team members implementing the class might not read said comment.

I think that above there are plenty of excellent examples of when/where final would be not only useful, but provide real value and real safety to the platform being built. It's no different for me. As I said, I don't know much about language design complexities but this is clearly a feature that people want/will use/I can't really see a way to abuse it or that it would generate bad code. I understand the concept of fearing scope creep but TS developers don't have to implement everything the community wants.

Just to add my bit to the debate, I would like to put my thumb on the yes-side of the scale because this issue is replete with good arguments supporting the addition of the feature, yet the arguments against that I see are:

None of the above seem like credible threats to the TS ecosystem itself, nor the code that is produced out of it; they all seem political (except maybe the efficiency one, but I have huge confidence in you guys so I'm not really worried about it, plus a minor efficiency decrease would be worth not blowing up important component/library constructs by unwitting inheritance mistakes).

Hope this is constructive! Love TS and want to see it keep improving!

👍

--edit--

In response to @RyanCavanaugh:

So what problem is being solved?

The problem being solved I think is outlined by many, many comments above. I don't mean this as an insult but that question does make it sound, to me, like you aren't actually paying attention to the arguments presented, and that you have your mind set on "no". I don't mean disrespect with that, its just that, honestly, this thread is full of examples of problems that this would solve.

Somehow I missed this comment, which very well outlines a serious concern for adding the feature and that makes sense to me. Again, I don't know the complexity of adding it but efficiency concerns aside (since it's not up to developers to worry about TS efficiency, that's the job of the TS team), there don't seem to be any good arguments against final (in my humblest of opinions).

cbutterfield commented 5 years ago

Most of the recent discussion has been centered on final methods (less so on final classes). Final methods are certainly my primary interest. I just noticed that if you expand the "this comment" link in @RyanCavanaugh 's previous post that he considers the final method discussion off-topic. However @mhegazy suggested that this ticket is the right place (when closing #9264). So as a lowly end-user I'm baffled as to where I should be asking for/voting for final methods. Guidance would be appreciated.

dudewad commented 5 years ago

@cbutterfield The contradiction is something that I noticed as well but did not point out in my post for fear of ranting (I actually found THIS thread through #9264 and came here at @mhegazy's direction). @RyanCavanaugh @mhegazy Per the comment above, where is the correct place to discuss final methods, because that actually is the thing I'm most interested in.

pelotom commented 5 years ago

Looking back on my heavily downvoted comment (make sure to go add your 👎 if you haven't yet!) I'm happy to report that React is no longer setting a bad example by forcing a class-based API on its users! With the Hooks proposal, the React team has come to their senses and embraced a radically simpler functional approach. With that, the last reason for many developers to have to use classes at all has evaporated, and good riddance. I'll reiterate: everything you think you need classes for can be done better without them.

The future of JS and TS is classless, praise be!

lucasbasquerotto commented 5 years ago

@cbutterfield I noticed that too, and if issue #9264 was closed to be discussed in this issue, I think the title should change to Suggestion: Final keyword for classes and methods, otherwise people looking for final methods (exclusively) might not add a reaction in the first post.

bbottema commented 5 years ago

Quoting @mhegazy: Java and/or C# uses the final class to optimize your class at runtime, knowing that it is not going to be specialized. this i would argue is the main value for final support. In TypeScript there is nothing we can offer to make your code run any better than it did without final.

I fundamentally disagree. I've developed in Java for many years in many teams and we have never used final classes or methods for runtime optimizations; in practice it is primarily a OOP design idiom. Sometimes I just don't want my class or method to be extended/overridden by external subclasses be it to protect and guarantee a function, or to limit a library's API noise to the essentials, or to protect API users from misunderstanding a complex API.

Yes those issues can be solved by adding comments or interfaces, but final is an elegant solution to reducing visible API to the necessary functions when all you want is a simple class with a clean extendable API.

Mcfloy commented 5 years ago

Even though this issue is nearly 3 years old, we really should have a final keyword as there's already an abstract keyword.

Keep in mind that we do not want to force people to use it and it's a feature as useful as the abstract keyword. But here's another usecase that will strongly show the benefit of a final keyword:


abstract class A {
  protected abstract myVar: string;

  protected abstract myFunction(): void;
}

class B extends A {
  protected readonly myVar: string = "toto";

  constructor() {
    super();
    this.myFunction();
  }

  protected myFunction() {
    console.log(this.myVar);
  }
}

class C extends B {
  constructor() {
    super();
  }

  protected myFunction() {
    console.log("tata");
  };

  public callFunction = () => {
    this.myFunction();
  }
}

const myB = new B(); // toto

const myC = new C(); // toto
myC.callFunction(); // tata

Result after compilation :

toto
toto
tata

So in this code, We have an abstract class A that have abstract methods and properties, we want the inherited class to define them, but we would like to not let any other classes to override those implementations. Best we could do would be to keep the protected keyword, but as you see, we can still redefine the attributes.

So what if we go further in the Typescript compilation process and use the readonly to protect our attributes (and pretend our methods are properties) ?

class B extends A {
  [...]

  protected readonly myFunction = () => {
    console.log(this.myVar);
  }
}

class C extends B {
  protected myVar: string = "I am not protected that much";
  [...]

  protected myFunction = () => { // using the readonly keyword doesn't prevent any modification
    console.log("tata"); // If you launch this code, tata will still be logged in the console.
  };
}

(Note that the code is compiled with tsc, no errors thrown when compiling or executing it) So now we have two problems :

At the moment, we could use decorators (sealed like the official Typescript documentation shows, or a final decorator ) but remember that it is only useful in runtime, and that we must prevent this in the compilation process instead.

I'll look if there's an issue about the "security breach" (if we can call that) when we want to override a protected readonly value and we actually can and we shouldn't. Otherwise I'll open an issue to debate on the verification of the readonly keyword among private, protected keyword with inheritance stuff.

tl ; dr : The final keyword would solve two problems (though the readonly verification would be a good start to prevent any unauthorized rewrite)

GuilhermeLessa commented 5 years ago

Three years later... it's so ridiculous.

pavel-castornii commented 5 years ago

In order to prove the need for this feature, I suggest to take a look what was done in other languages:

Java:
final class SomeClass { ... }
PHP:
final class SomeClass { ... }
C#:
sealed class SomeClass {...}
C++:
class SomeClass final { ... }
Delphi:
type SomeClass = class sealed() ... end;

So, we see that in all the most popular OOP languages this feature exists because it comes from the inheritance logic of OOP.

Mcfloy commented 5 years ago

Also I have to quote the Dev lead of TS saying

if we have final classes, then we'll "need" final methods or properties as well.

I'm not sure if it's ironic, but in JS the readonly keyword is used for properties (and methods if you cheat), so that's a pretty dumb point.

And maybe not letting one guy closing the issue when he have +40 downvotes meaning the community disagree strongly with him would be a good advice to avoid further dumb situations.

If you're not interested in reinforcing the main feature of Typescript, please say it loud so that tech news can relay that on Twitter, because otherwise, it's a just an hidden bad buzz. You're just ignoring your community, AND you have no process in making the community validating what we need or not.

RyanCavanaugh commented 5 years ago

maybe not letting one guy closing the issue when he have +40 downvotes meaning the community disagree strongly with him would be a good advice to avoid further dumb situations

I'm not "one guy". When I make decisions here, it is the reflection of the entire TypeScript team's decision-making process. The design team has looked at this issue multiple times, and each time come to the same conclusion. You are welcome to disagree with that outcome, but the process is not up for debate.

you have no process in making the community validating what we need or not.

This is the process, right here: You calling my summarization of our reasoning "dumb" and me (and other people on the team) reading it. We're listening to the feedback.

asimonf commented 5 years ago

I know pointing to other implemented features as reasons for considering this one is a smell and not really an argument, but I find it ironic that a type aliasing mechanism was considered and added (it may well be awesome, but one could certainly live without it) but something like this is rejected.

In the end, I can live without such a feature, but the same could be said for about half the features TS has. So, then, what would be an appropriate reason to consider implementing this?

RyanCavanaugh commented 5 years ago

GitHub unhelpfully hides a lot of comments here, so I'm reposting various responses (plus some additions) we've given in the past. Adding some thoughts in below as well.


Some points were considered when we reviewed this last time


haven't heard a convincing argument for why TS doesn't need it

Just a reminder that this is not how it works. All features start at -100. From that post

In some of those questions, the questions asks why we either “took out” or “left out” a specific feature. That wording implies that we started with an existing language (C++ and Java are the popular choices here), and then started removing features until we got to a point where we liked. And, though it may be hard for some to believe, that's not how the language got designed.

We have a finite complexity budget. Everything we do makes every next thing we do 0.1% slower. When you request a feature, you're requesting that every other feature become a little bit harder, a little bit slower, a little bit less possible. Nothing gets in the door here because Java has it or because C# has it or because it'd only be a few lines of code or you can add a commandline switch. Features need to pay for themselves and for their entire burden on the future.


What would change our mind here? People showing up with examples of code that didn't work the way it should have because the final keyword was missing.

The scenario here that the keyword attempts to address is one where someone mistakenly inherits from a class. How do you mistakenly inherit from a class? It's not an easy mistake to make. JavaScript classes are inherently fragile enough that any subclass must understand its base class's behavioral requirements, which does mean writing some form of documentation, and the first line of that documentation can simply be "Do not subclass this class". Or there can be a runtime check. Or the class constructor can be hidden behind a factory method. Or any other number of options.

In other words: the only correct process for inheriting from a class in JavaScript always starts with reading that class's documentation. There are too many constraints the base class might have to simply derive and start coding. If, at the very first step, you should already know not to try, what value is the static enforcement providing at step three?

Why must TypeScript have this keyword when the situation is already one in which you should have had encountered at least two separate roadblocks telling you not to be there to begin with?

I've been programming for approximately forever years now and I have not once tried to subclass something only to find out it was sealed and I shouldn't have tried. It's not because I'm a super genius! It's just an extremely uncommon mistake to make, and in JavaScript these situations are already ones where you need to be asking questions of the base class in the first place. So what problem is being solved?

If you're not interested in reinforcing the main feature of Typescript, please say it loud so that tech news can relay that on Twitter

We're very explicit about how we design the language; non-goal number one speaks exactly to this suggestion.


My personal reaction after reading the many comments here is that there's a strong biasing effect due to constructs that exist in other languages. If you just approach this from a blank slate perspective, there are dozens of behavioral you could imagine, of which TypeScript has only a small subset.

You could easily imagine some keyword that was applied to a method and enforced that derived methods called it via super. If C# and Java had this keyword, people would absolutely apply that keyword in places where it made sense. In fact, it'd arguably be much more commonly enforced than final, because it's impossible to apply a runtime check for, and is a much more subtle aspect of the base-derived contract than "derived classes may not exist". It'd be useful in a variety of ways that final wouldn't be. I would much rather have that keyword than this one (though I think neither meets the complexity vs value bar).

So, then, what would be an appropriate reason to consider implementing this?

When we look at feedback, strong considerations look like this:

final hits these, but so would a modifier that says "This function can't be called twice in a row" or "This class's construction doesn't really finish until the next async tick". Not everything imaginable should exist.

We've never seen feedback like "I spent hours debugging because I was trying to inherit from a class that wasn't actually subclassable" - someone saying that would rightly trigger a 'wat' because it's not really imaginable. Even if the modifier existed, it wouldn't really help the situation, because libraries don't document if classes are intended to be final - e.g. is fs.FSwatcher final ? It seems like even the node authors don't know. So final is sufficient if the authors know that it's final, but that's going to be documented regardless, and a lack of final doesn't really tell you anything because it's often simply not known either way.

Mcfloy commented 5 years ago

I read the entire block and understand the statement of the team on the choice of features, I'll just go back on certain points from my comments

I'm not "one guy". When I make decisions here, it is the reflection of the entire TypeScript team's decision-making process.

Sorry I was refering to mhegazy and the pretty massive feedback he got from the community that uses Typescript.

If you really feel like classes are completely useless without final, that is something to take up with the TC39 committee or bring to es-discuss. If no one is willing or able to convince TC39 that final is a must-have feature, then we can consider adding it to TypeScript.

I do not think final is the first step as there's already a proposal for the private keyword https://github.com/tc39/proposal-private-methods

I'm skeptical about the You should a comment to say *dont do this*, it's like saying on a form Hey don't write down specific characters, you coded for nearly forever years so you should know the n°1 rule of a dev: Never trust the user (and the dev that will use your code)

I'm not saying we must absolutely stop everything and put a billion dollars to implement the final keyword, because the usecases are too low to be that efficient, also consider that I gave few exemples where the limit are both from TS and JS and that if people choose TS, it's to prevent the errors in compile-time, not in run-time. If we want things to blow up at runtime, we can use JS and stop caring about TS, but that's not the point, because there's an ultimate usecase that shows how I use the final keyword : I want to lock a method, I don't want anyone to override it.

And as Javascript is limited by that, the community thought Typescript could go beyond that limit, that's why this issue has been on for 3 years, that's why people are still wondering why this feature is not here, and that's why we want the compiler to do the manual checking of a class and/or method.

You didn't wait JS to have private/public methods to implement them, though there are actually proposals to include them with the # keyword (less verbose than public/private), I know you wanted to create a perfect language because perfection is not when you can't add anything anymore, it's when you can't remove anything anymore.

If you can find a solution to prevent a method to be overwritten in the compile process (and not in runtime-process as the point wouldn't be valid), be my guest.

I tried to show the weakness of the situation (on methods/properties and not class), because any TS dev can rewrite any libraries they want, breaking anything they want, maybe that's the beauty of the thing, I guess.

Thanks for the reply by the way, no hate or bad behavior intended against the dev team or you.

thorek commented 5 years ago

All valid points! But, please let’s split the discussion between

a) Support final classes b) Support final methods

While all your arguments target a) - none targets b.

Having final methods is crucial as was pointed out many times in the discussion. The only answer I heard so far was „don’t do OOP“ - but that’s nothing I (and many other) would go with.

Am 28.01.2019 um 20:32 schrieb Ryan Cavanaugh notifications@github.com:

GitHub unhelpfully hides a lot of comments here, so I'm reposting various responses (plus some additions) we've given in the past. Adding some thoughts in below as well.

Some points were considered when we reviewed this last time

CLASSES ARE A JAVASCRIPT FEATURE, NOT A TYPESCRIPT FEATURE. If you really feel like classes are completely useless without final, that is something to take up with the TC39 committee or bring to es-discuss. If no one is willing or able to convince TC39 that final is a must-have feature, then we can consider adding it to TypeScript. If it's legal to invoke new on something, that has a one-to-one runtime correspondence with inheriting from it. The notion of something that can be new'd but not super'd just doesn't exist in JavaScript We are trying our best to avoid turning TypeScript into an OOP keyword soup. There are many better composition mechanics than inheritance in JavaScript and we don't need to have every single keyword that C# and Java have. You can trivially write a runtime check to detect inheritance from a "should-be-final" class, which you really should do anyway if you're "worried" about someone accidently inheriting from your class since not all your consumers might be using TypeScript. You can write a lint rule to enforce a stylistic convention that certain classes aren't inherited from The scenario that someone would "accidently" inherit from your should-be-sealed class is sort of an odd one. I'd also argue that someone's individual assessment of whether or not it's "possible" to inherit from a given class is probably rather suspect. There are basically three reasons to seal a class: Security, performance, and intent. Two of those don't make sense in TypeScript, so we have to consider the complexity vs gain of something that's only doing 1/3rd the job it does in other runtimes. haven't heard a convincing argument for why TS doesn't need it

Just a reminder that this is not how it works. All features start at -100 https://blogs.msdn.microsoft.com/ericgu/2004/01/12/minus-100-points/. From that post

In some of those questions, the questions asks why we either “took out” or “left out” a specific feature. That wording implies that we started with an existing language (C++ and Java are the popular choices here), and then started removing features until we got to a point where we liked. And, though it may be hard for some to believe, that's not how the language got designed.

We have a finite complexity budget. Everything we do makes every next thing we do 0.1% slower. When you request a feature, you're requesting that every other feature become a little bit harder, a little bit slower, a little bit less possible. Nothing gets in the door here because Java has it or because C# has it or because it'd only be a few lines of code or you can add a commandline switch. Features need to pay for themselves and for their entire burden on the future.

What would change our mind here? People showing up with examples of code that didn't work the way it should have because the final keyword was missing.

The scenario here that the keyword attempts to address is one where someone mistakenly inherits from a class. How do you mistakenly inherit from a class? It's not an easy mistake to make. JavaScript classes are inherently fragile enough that any subclass must understand its base class's behavioral requirements, which does mean writing some form of documentation, and the first line of that documentation can simply be "Do not subclass this class". Or there can be a runtime check. Or the class constructor can be hidden behind a factory method. Or any other number of options.

In other words: the only correct process for inheriting from a class in JavaScript always starts with reading that class's documentation. There are too many constraints the base class might have to simply derive and start coding. If, at the very first step, you should already know not to try, what value is the static enforcement providing at step three?

Why must TypeScript have this keyword when the situation is already one in which you should have had encountered at least two separate roadblocks telling you not to be there to begin with?

I've been programming for approximately forever years now and I have not once tried to subclass something only to find out it was sealed and I shouldn't have tried. It's not because I'm a super genius! It's just an extremely uncommon mistake to make, and in JavaScript these situations are already ones where you need to be asking questions of the base class in the first place. So what problem is being solved?

If you're not interested in reinforcing the main feature of Typescript, please say it loud so that tech news can relay that on Twitter

We're very explicit about how we design the language https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals; non-goal number one speaks exactly to this suggestion.

My personal reaction after reading the many comments here is that there's a strong biasing effect due to constructs that exist in other languages. If you just approach this from a blank slate perspective, there are dozens of behavioral you could imagine, of which TypeScript has only a small subset.

You could easily imagine some keyword that was applied to a method and enforced that derived methods called it via super https://github.com/Microsoft/TypeScript/issues/21388. If C# and Java had this keyword, people would absolutely apply that keyword in places where it made sense. In fact, it'd arguably be much more commonly enforced than final, because it's impossible to apply a runtime check for, and is a much more subtle aspect of the base-derived contract than "derived classes may not exist". It'd be useful in a variety of ways that final wouldn't be. I would much rather have that keyword than this one (though I think neither meets the complexity vs value bar).

So, then, what would be an appropriate reason to consider implementing this?

When we look at feedback, strong considerations look like this:

I can't adequately express the behavior of this JavaScript library My code compiled, but really shouldn't have because it had this (subtle) error The intent of this code is not adequately communicated by the type system final hits these, but so would a modifier that says "This function can't be called twice in a row" or "This class's construction doesn't really finish until the next async tick". Not everything imaginable should exist.

We've never seen feedback like "I spent hours debugging because I was trying to inherit from a class that wasn't actually subclassable" - someone saying that would rightly trigger a 'wat' because it's not really imaginable. Even if the modifier existed, it wouldn't really help the situation, because libraries don't document if classes are intended to be final - e.g. is fs.FSwatcher https://nodejs.org/api/fs.html#fs_class_fs_fswatcher final ? It seems like even the node authors don't know. So final is sufficient if the authors know that it's final, but that's going to be documented regardless, and a lack of final doesn't really tell you anything because it's often simply not known either way.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/8306#issuecomment-458268725, or mute the thread https://github.com/notifications/unsubscribe-auth/AA3NA4o9wu54CcJyK1C8WHVjXIQwfmsUks5vH1A8gaJpZM4IP8M3.

bbottema commented 5 years ago

Yeah, the discussion is so diffuse now. I would like to see these issues in a tracker where you can vote, like the one used for IntelliJ / webstorm issues. Get some actual numbers of how many people would like to see final for classes and/or methods.

leandrit1 commented 5 years ago

final would be very helpful

david0u0 commented 5 years ago

An abstract keyword is used to remind programmers that they should override the field. A keyword to remind them not to override it will be helpful in similar way.

ProjectCleverWeb commented 5 years ago

Final methods can be crucial to making code more structured and/or secure.

For example, lets say you have an application that supports 3rd party plugins. You can force all plugins to extend an abstract class that implements an interface for any methods they MUST create and contains a few final methods that control the order of operations and allow you to implement hooks between operations. Because of this, your application doesn't need to be aware of the specifics of any individual plugin, and still be aware of some of its internal state.

Without final methods, they could interfere with your functions controlling the order of operations; potentially causing a bug, exploit, or other unexpected behavior within your application. While most situations will have some type of workaround, those workarounds can be complicated, repetitive, and sometimes unreliable; whereas final methods make it one simple solution when the method is defined.

Also, While this example is specific to plugins, it is applicable to other situations as well. (new dev on team, ensuring certain logic is not run out of scope, standardizing how code is run, making sure security based methods are not changed, etc. all could benefit from this)