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();
    }
}
mhegazy commented 8 years ago

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

Zorgatone commented 8 years ago

From what I recalled I was sure the compiler didn't like the private keyword on the constructor. Maybe I'm not using the paste version though

mhegazy commented 8 years ago

This is a new feature, will be released in TS 2.0, but you can try it using typescript@next. see https://github.com/Microsoft/TypeScript/pull/6885 for more details.

Zorgatone commented 8 years ago

Ok thank you

duanyao commented 8 years ago

Doesn't private constructor also make a class not instantiatable out of the class? It's not a right answer to final class.

mhegazy commented 8 years ago

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. Consider using comments to inform your users of the correct use of the class, and/or not exposing the classes you intend to be final, and expose their interfaces instead.

0815fox commented 8 years ago

I do not agree with that, instead I agree with duanyao. Private does not solve that issue, because I also want classes which are final to be instanciateable using a constructor. Also not exposing them to the user would force me to write additional factories for them. For me the main value of final support is, that it prevents users from making mistakes. Arguing like that: What does TypeScript offer to make my code run faster, when I use types in function signatures? Isn't it also only for preventing users from making mistakes? I could write comments describing which types of values a user should pass in as a parameter. It's a pitty, that such extensions like a final keyword are just pushed away, because on my opinion it collides with the original intension of typescript: make JavaScript safer by adding a compilation level, which performs as many checks as possible to avoid as many mistakes upfront as possible. Or did I misunderstand the intention of TypeScript?

0815fox commented 8 years ago

There should also be a final modifier for methods:

class Foo {
  final fooIt():void{

  }
}

class Bar {
  fooIt():void {

  }
}
// => Method fooIt of Bar cannot override fooIt of Foo, because it is final.

E.g. I often use following pattern, where I want to urgently avoid fooIt to be overridden:

import Whatever ...

abstract class Foo {
  private ImportantVariable:boolean;

  protected abstract fooIt_inner:Whatever();

  public final fooIt():Whatever() {
    //do somestate change to aprivate member here, which is very crucial for the functionality of every Foo:
    ImportantVariable = true;
    //call the abstract inner functionality:
    return this.fooIt_inner();    
  }
}
mhegazy commented 8 years ago

The argument about cost vs. utility is a fairly subjective one. The main concern is every new feature, construct, or keyword adds complexity to the language and the compiler/tools implementation. What we try to do in the language design meetings is to understand the trade offs, and only add new features when the added value out weights the complexity introduced.

The issue is not locked to allow members of the community to continue adding feedback. With enough feedback and compelling use cases, issues can be reopened.

mindarelus commented 8 years ago

Actually final is very simple concept, does not add any complexity to the language and it should be added. At least for methods. It adds value, when a lot of people work on a big project, it is valuable not to allow someone to override methods, that shouldn't be overridden.

mcdirmid commented 8 years ago

In TypeScript there is nothing we can offer to make your code run any better than it did without final.

Wow, cringe! Static types don't make your code run any better either, but safety is a nice thing to have.

Final (sealed) is right up there with override as features I'd like to see to make class customizations a bit safer. I don't care about performance.

pauldraper commented 8 years ago

Static types don't make your code run any better either, but safety is a nice thing to have.

Exactly. Just as private prvents others from calling the method, final limits others from overriding the method.

Both are part of the class's OO interface with the outside world.

timmeeuwissen commented 8 years ago

Completely agree with @pauldraper and @mindarelus. Please implement this, this would make a lot of sense I really miss it currently.

aluanhaddad commented 8 years ago

I don't think final is only beneficial for performance, it's also beneficial for design but I don't think it makes sense in TypeScript at all. I think this is better solved by tracking the mutability effects of Object.freeze and Object.seal.

0815fox commented 7 years ago

@aluanhaddad Can you explain that in more detail? Why do you think it does not "make sense in TypeScript at all"? Freezing or sealing object means to disallow adding new properties to an object, but does not prevent adding properties to a derived object, so even if I would seal the base class I could still override the method in a child class, which extends that base class. Plus I could not add any properties to the base class at runtime.

hk0i commented 7 years ago

The idea of using final on a class or class method in java has more to do with minimizing mutability of the object for thread safety in my opinion. (Item 15. Joshua Bloch, Effective Java)

I don't know if these principals carry over into javascript seeing as everything in JS is mutable (correct me if I'm wrong). But Typescript is not Javascript, yeah?

I would really like to see this implemented. I think it'll help create more robust code. Now... How that translates into JS, it honestly probably doesn't have to. It can just stay on the typescript side of the fence where the rest of our compile-time checking is.

Sure I can live without it, but that's part of what typescript is, right? Double checking our overlooked mistakes?

rylphs commented 7 years ago

To me final would play the same role in typescript as private or typings, that is code contract. They can be used to ensure your code contract don't get broken. I would like it so much.

cloverich commented 7 years ago

@hk0i its also mentioned in Item 17 (2nd edition) in a manner similar to what's been echoed here:

But what about ordinary concrete classes? Traditionally, they are neither final nor designed and documented for subclassing, but this state of affairs is danger- ous. Each time a change is made in such a class, there is a chance that client classes that extend the class will break. This is not just a theoretical problem. It is not uncommon to receive subclassing-related bug reports after modifying the internals of a nonfinal concrete class that was not designed and documented for inheritance.

The best solution to this problem is to prohibit subclassing in classes that are not designed and documented to be safely subclassed. There are two ways to prohibit subclassing. The easier of the two is to declare the class final. The alternative is to make all the constructors private or package-private and to add public static factories in place of the constructors.

I would argue it does not increase the cognitive complexity of the language given that the abstract keyword already exists. However, I cannot speak to the implementation / performance impact of it and absolutely respect protecting the language from that angle. I think separating those concerns would be fruitful towards deciding whether or not to implement this feature.

MrDesjardins commented 7 years ago

I believe that final would be an excellent addition to seal a class. One use case is that you may have a lot of public methods in your class but expose, through an interface, just a subset of them. You can unit tests the implementation quickly since it has all these public methods while the real consumer uses the interface to limits access to them. Being able to seal the implementation would ensure that no one extends the implementation or change public methods.

You may also ensure that no one is inheriting your class. TypeScript should be there to enforce those rules, and the suggestion about commenting seems to be a lazy approach to solve this use case. The other answer I read is about using private which is only suitable for a particular situation but not the one I explained above.

Like many people in this thread, I would vote to be able to seal class.

MrMatthewLayton commented 6 years ago

@mhegazy Private constructors and sealed/final classes have very different semantics. Sure, I can prevent extension by defining a private constructor, but then I also can't call the constructor from outside the class, which means I then need to define a static function to allow instances to be created.

Personally I'd advocate for having sealed/final compiler checks to ensure that classes and methods marked sealed/final cannot be extended or overridden.

In the context of my problem, I want to be able to have a public constructor so I can instantiate the object, but prevent extension of the class, and only the addition of sealed/final will allow that.

ghost commented 6 years ago

There is a task - write code that

And final keyword is necessary here, IMHO.

zigszigsdk commented 6 years ago

I see final as a great way to remind yourself and users of your code which protected methods they should be overriding, and which they shouldn't. As a sidenote, I'm also a proponent of explicitly stating that you're overriding, partially so that the reader knows, but also so that the transpiler complains if you typo the method's name.

aluanhaddad commented 6 years ago

@zigszigsdk Since methods are never overridden, how would this work?

zigszigsdk commented 6 years ago

For final: The same way as it works now, except the transpiler would complain if one hides a super's method -which has been declared final- from the this context.

For override: The same way it works now, except the transpiler would complain if one declares a method override, and its super doesn't have a method by that name, or it does have one but it's declared to be final. It could possibly also be warning you if you hide a super's method from the this context and don't state override.

sbichenko commented 6 years ago

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.

@mhegazy Not quite! Another important function is to be explicit about which parts of the class expect to be overriden.

For example, see item 17 in "Effective Java": "Design and document for inheritance or else prohibit it":

It is not uncommon to receive subclassing-related bug reports after modifying the internals of a nonfinal concrete class that was not designed and documented for inheritance. The best solution to this problem is to prohibit subclassing in classes that are not designed and documented to be safely subclassed. There are two ways to prohibit subclassing. The easier of the two is to declare the class final.

Same goes for final methods, in my opinion. It's very rare that a class is designed to support overriding any of its public methods. This would require very intricate design and a huge amount of testing, because you'd have to think of every possible combination of states that might result from combination of non-overrided and overriding behaviour. So instead a programmer might declare all public methods final except one or two, which would decrease the number of such combinations dramatically. And more often that not, one or two overridable methods is precisely what's needed.

jazzfog commented 6 years ago

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

legend80s commented 6 years ago

Another compelling user case @mhegazy . Today I learned the Template Method Pattern, and found that final is required to prohibit subclasses changing the template method as the Template Method Pattern in wikipedia says. But I cannot do this in my lovely TypeScript. What a pity!

Template pattern lets subclasses redefine certain steps of an algorithm without changing the algorithm's stucture

lucasyvas commented 6 years ago

For me it's as simple as trying to enforce composition in cases over inheritance. Without final you can't do this.

hk0i commented 6 years ago

As much as I'd also like to see final come to typescript, I think the underlying message that Microsoft is trying to send us is that if we want final, we should use a language that supports it.

TheColorRed commented 6 years ago

I would like to see this issue re-opend and implemented.

When building a library that you are going to use internally or share publicly (such as on npm), you don't want those who use it to have to browse the code and search through comments or docs to see if the class can/can't be overwritten. If they overwrite it throw an error.

In my code, I have a class that gets extended, and when some sort of event takes place it triggers a method. If the sub-class defines the method it will that one otherwise it will fall back to the default one. However there are also other methods in the class that are not "fallback" methods, they are more of helper methods such as adding an item to the DOM in which case I don't want these methods to get overwritten.

davidmpaz commented 6 years ago

My 5 cents on the topic are that this should be re-opened and implemented.

I would use it to enforce clients of libraries to make their own concrete classes and not extends from some other existing already. Like @lucasyvas said, favor composition. Or just enforce new concrete class implementation to provide to the Dependency Injection in angular for example.

Xample commented 6 years ago

I also vote for re-opening, with support for both classes and methods

niokasgami commented 6 years ago

I don't say that we shouldn't I am just unsure of how the pattern of the final keyword would work in the context of typescript?

wouldn't the final keyword stop users/ programmer to override/ inherit from the said class/method?

My way of seing Final is that it's "Freeze" the data which mean it's can't be changed anymore. I mean this can be nice to use it's although can be truly an hassle for peoples who want to "bug" fix or change some behavior.

Xample commented 6 years ago

@niokasgami Freeze is related to [data] (https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Object/freeze) and already available as part of ES6. Final applies to classes and methods (variable can be readonly and therefore act as "final" as we cannot override a super variable).

The "Final" keyword is used in JAVA where sealed is in C#.

As typescript is strongly inspired by C#, there should be a higher probability that "sealed" will be the chosen one. Now, we could possibly have confusion with the ES6 seal which basically seals the object at runtime?

TheColorRed commented 6 years ago

@Xample I think you can also use final on local variables in Java. I remember having to do it when I was writing an android app. I think it required me to do it with an event but I am not 100% certain.

function(){
    let final myVariable = 'Awesome!'
    document.addEventListener('scroll', e => {
        console.log(myVariable)
        myVariable = 'Not Awesome' // Throws an error
    })
}
eternalphane commented 6 years ago

@TheColorRed Well, final local variable in Java means constant, isn't it?

TheColorRed commented 6 years ago

@EternalPhane I guess so, I have always used const in a global like scope... Never thought of using it within a function or method... I guess me previous comment is void then...

niokasgami commented 6 years ago

@Xample Yeah I see your point for Sealed from the C# it's not working as the same Honestly in my opinion BOTH keyword seal and final couldn't really enter the category since both can be confusing (sealed for seal etc) and Final we are unsure how to apply it for since in Java it is used as an constant :/

hk0i commented 6 years ago

In java you can final any variable, method or class. For variables it means the references cannot be reassigned (for non-reference types this means you cannot change their values either). For classes it means no extensions (extends... keyword). For methods it means no overriding in subclasses.

Main reasons to final in java:

I may have left out a couple of use cases but I was trying to keep it very general and give some examples.

@TheColorRed Android requires it when you declare a variable outside of a callback (anonymous class) and then reference it inside. I think this aligns with thread safety issues again. I think they are trying to stop you from reassigning the variable from another thread.

Despite its usefulness I don't think we will see this feature coming any time soon. It might be a good time to look at other solutions or, as in my case, drop typescript all together and deal with the sloppiness in JS. Javascript has a lot of "benefits" by not having any of this type hinting or anything and ultimately your code will be converted to JS anyway. If you want browser compatibility you can write ES6 and use Babel to convert it.

I think realistically even though you can give a slap on the wrist for some of these benefits, you don't get any of the real benefits of the java-esque final once it's been converted to js.

niokasgami commented 6 years ago

@hk0i I think I could understand your point but overall I agree and kinda disagree in the same time about your opinion.

Most of the code in typescript are I would say for debugging/tracking purpose for helping the programmer to well I guess do "clean" code (please don't quote me on that I know they have some typescript spaghetti code out here hahah)

most of typescript feature doesn't get transpilled most of the time when converted to JS because they just don't exists in JS.

Although when you runtime/ write your code it's help to see error in your code and to track it more easily than with JS (and HECK JS can be hard to track bugs lol)

So I think the use of the final keyword could be seing as an blocker for API user to know : Oh okay this class shouldn't be extended since it's extension could provoke weird behavior etc...

Typescript as good language it is I find myself sad to find that it lack some important feature such "true" static class keyword who stop peoples from calling the class constructor but still being able to access it.

Xample commented 6 years ago

@hk0i I don't agree neither, the purpose of typescript is to structure the code and therefore providing the tools to do so. We do not HAVE to use final in Java, we typically use it when we know that extending a class or method could lead to introducing side effects (or for any other safety reason).

Relying on the current ES6 solutions adding a runtime mutability check (saying using Object.seal or Object.freeze) makes you loose the benefit of having errors directly when you type your code (despite it could easily be done using this decorator).

Xample commented 6 years ago

@TheColorRed yes… and in typescript we have a distinction for local and class variable.

None of them would be accurate for method or class. Overriding a method does not mean we are redefining it (think in virtual methods in C++), we just define a method will will be primary be called before the super one (which we can call using super).

Xample commented 6 years ago

@niokasgami perhaps the naming is not really a problem as we will still understand that Object.seal applied to… an object while sealing a class and a method to the structure itself.

TL;DR:

By the way I forgot to mention there is also Object.preventExtensions() Differences can be seen here

reading

updating and deleting

creating

extending

overriding

BONUS: As sealing a method is usually to prevent people overriding mandatory super code, I made the proposal of forcing people to call the super method (the same behaviour as for the constructor but for any method) don't hesitate to vote up if you think it would be useful.

hk0i commented 6 years ago

Don't get me wrong, I'm not against this. I'm very much for it. I'm just trying to get behind Microsoft's paradoxical mindset of why it shouldn't be a feature, which is essentially saying that because it's not a JavaScript feature it cannot be a TypeScript feature.

... If I'm understanding correctly.

TheColorRed commented 6 years ago

@hk0i Generics, readonly, and many other features are not a JavaScript features, but they are still added into TypeScript, so I don't think that is the reason...

pelotom commented 6 years ago

TypeScript only shies away from features which require generating code in a non-straightforward way. Generics, readonly, etc are purely compile-time notions which can be simply erased to produce the generated output, so they’re fair game.

TheColorRed commented 6 years ago

final can be "erased" at compile time too, so why doesn't that make it "fair game"?

pelotom commented 6 years ago

🤷‍♂️

niokasgami commented 6 years ago

@TheColorRed I think this more or less this either they have other priority to integrate. Or they unsure how predictable this new keyword could work on compile/ coding time. if you think about it the design of final keyword can be extremelly vague. final could be used a lots for many purpose. As you can see from the Jsdoc documentations you can use the tag "@final" which tell the jsdoc interpreter that this variable is frozen thus readonly.

here a design clash. Readonly is aplicable on variable and getter if I recall which "froze" the variable and make it readonly and final make it final thus readonly as well.

this can cause confusion to the programmer who is unsure if they should tag their variables final or readonly.

UNLESS we reserve this keyword exclusively for class and method declarations, I think the behavior or final would clash with readonly.

I think instead of sealed (which can clash with object.seal) or final (which this purpose design clash readonly keyword) we could go with a more "direct" way to name and design it.

take note this merelly a "idea" of a behavior who is similar but in same time different from the C# behavior? I took some inspirations from how C# is normally nonOverridable and then you have to say that this method is virtual.

` namespace Example {

export class myClass {

    constructor(){}

    // Make the func non ovveridable by inheritance. 
    public unoveridable myMethod(){

    }

    public myMethodB(){

    }
}

export class MyClassB extends myClass {

    // Nope not working will throw an error of an nonOverridable error
    myMethod(){
        super.myMethod();
    }

    // Nope will not work since unoverridable.
    myMethod(){

    }

    // Design could be implemented differently since this could complicate  ¸
    // the pattern of typescript
    public forceoverride myMethod(){
      // destroy previous pattern and ignore the parent method thus a true ovveride
    }

    // Yup will work since it's still "virtual".
    myMethodB(){
        super.myMethodB();
    }
}
// Can extend an existing Parent class
// Declaring an unextendable class make all is component / func nonOverridable by 
// inheritance. (A class component can still be overwritten by prototype usage.)
export unextendable class MyFinalClass extends Parent {

    constructor()

    // nonoverridable by default.
    public MyMethod(){

    }
}
// nope throw error that MyFinalClass is locked and cannot be extended
export class MyChildClass extends MyFinalClass{}

}`

Edit : I didn't include Variable and get since readonly already exists.

bcherny commented 6 years ago

@mhegazy Please consider reopening this issue. The community response seems to be overwhelmingly on the side of adding a final keyword, like Java (final), C# (sealed, readonly), PHP (final), Scala (final, sealed), and C++ (final, virtual) have. I can't think of a statically typed OOP language that doesn't have this feature, and I haven't heard a convincing argument for why TS doesn't need it.