-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support final classes (non-subclassable) #8306
Comments
a class with private constructor is not extendable. consider using this instead. |
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 |
This is a new feature, will be released in TS 2.0, but you can try it using |
Ok thank you |
Doesn't private constructor also make a class not instantiatable out of the class? It's not a right answer to final class. |
Java and/or C# uses the |
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. |
There should also be a
E.g. I often use following pattern, where I want to urgently avoid fooIt to be overridden:
|
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. |
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. |
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. |
Exactly. Just as Both are part of the class's OO interface with the outside world. |
Completely agree with @pauldraper and @mindarelus. Please implement this, this would make a lot of sense I really miss it currently. |
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 |
@aluanhaddad Can you explain that in more detail? Why do you think it does not "make sense in TypeScript at all"? |
The idea of using 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? |
To me |
@hk0i its also mentioned in Item 17 (2nd edition) in a manner similar to what's been echoed here:
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. |
I believe that 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. |
What does typescript exactly do in runtime for optimizing?????? All about preventing bugs. |
Sadly, over a number of years, the powers that be constantly "misinterpret"
a request that is primary related to writing safe and reliable code as
something to do with performance. It is quite bizarre.
I think the powers that be got offended early on in the process and now
routinely reject anything with the word final in it (e.g. final methods, final classes) as an affront to their
dignity.
I can't really imagine any other explanation for the refusal to consider
final other than a psychological explanation. It's hard to believe that
the highly skilled and intelligent maintainers would actually misinterpret
the request over and over and over again.
After all the primary motivation with typescript has nothing to do with
performance optimization but rather writing safe and reliable code.
This post is mainly for newbies to set their expectations. I expect no
change whatsoever by the maintainers because they are clearly not assessing
this on technical merit
Regards
Charlie
…On Fri, Sep 9, 2022, 1:41 PM SoR ***@***.***> wrote:
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.
What does typescript exactly do in runtime for optimizing??????
All about preventing bugs.
—
Reply to this email directly, view it on GitHub
<#8306 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAC2EG662YSLPPYUGUWW4A3V5NZGLANCNFSM4CB7YM3Q>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Instead of riding this dead horse switch over to the well written retry of this whole issue in #50532, give it your whole support at least with thumbs-up reactions and hope this issue is not blindly closed for the wrong reasons again. |
Initializing members of classes consistently reduces the number of “shapes”, preventing some deoptimization/bailout scenarios. While this is not the primary goal of TypeScript, it does make sense for TypeScript to avoid features which can not be translated into efficient JavaScript equivalents. For example, I would not expect TypeScript to add a concise syntax for an operator which implements a deep equality check that would require full object traversal. That belongs in a library and adding an operator would make the operation appear lightweight to the developer while being heavyweight.
I wouldn’t say this so strongly, but I do agree. TypeScript lead the way with JavaScript language innovation by introducing an implementation of It’s understandable that they don’t want to risk making new syntax which conflicts with future changes to ECMAScript. But it comes at the cost of preventing people from expressing certain constraints which would be very beneficial in a static type checker. It is a balance, so a line needs to be drawn somewhere, but it seems to be drawn in a different place than it was when TypeScript was first created, unfortunately. So… are there any good TypeScript forks? Or is the ecosystem forcing us to stay? Hrm… |
Does this help? (At the declare const _: unique symbol;
type NoOverride = { [_]: typeof _; }
class A {
readonly baz: string & NoOverride = '' as any;
// Note - `ReturnType & NoOverride`
foo(): { a: string } & NoOverride {
return { a: '' } as any;
}
// if this function return nothing, use `NoOverride` only
bar(): NoOverride {
console.log(0);
return null!;
}
}
class B extends A {
// @ts-expect-error - Type 'string' is not assignable to type 'NoOverride'.
baz = '';
// @ts-expect-error - Property '[_]' is missing in type '{ a: string; }' but required in type 'NoOverride'.
foo() {
return { a: '' };
}
// @ts-expect-error - Type 'void' is not assignable to type 'NoOverride'.
bar() {
}
} |
@Max10240 doesn't that break external module doing |
Yes. The above answers only apply to the |
The ES proposal predated TypeScript implementation by over a year and a half.
Classes were included in ES4/Harmony, years before TS existed. Better examples would be decorators or private members. Especially private members; TS added them as a type-level feature, but then JS added private members as a language/runtime feature. It's possible (but unlikely) that JS would do something similar with final classes. All that said, TS supporting final classes + nominal tying would be very useful. |
Sometimes TypeScript design decisions surprise me. How can you both have Kinda the same vibes as "not generating anything at runtime" but also having had both the decorators and int-backed enums generate extra code. |
This comment was marked as off-topic.
This comment was marked as off-topic.
I ran into an issue stemming from the lack of Some languages are coming out with the Here's an incredibly simplified version of what I was trying to do: //////////////////
// model.ts
export abstract class Base {
static factory(): Base {
// Some logic to return either a Foo or a Bar
}
abstract isFoo(): this is Foo;
abstract isBar(): this is Bar;
}
export class Foo extends Base {
constructor(public a: string) { super(); }
override isFoo(): this is Foo { return true; }
override isBar(): this is Bar { return false; }
}
export class Bar extends Base {
constructor(public b: number) { super(); }
override isFoo(): this is Foo { return false; }
override isBar(): this is Bar { return true; }
}
//////////////////
// index.ts
const val = Base.factory();
if (val.isFoo()) {
console.log(val.a); // Can access `a` because `val` is now Foo
} What I'd like to do is be able to tell the compiler that if if (val.isFoo()) {
console.log(val.a); // Can access `a` because `val` is now Foo
} else {
console.log(val.b); // Error: even though the only other subtype of `Base` that I defined was `Bar`, the compiler can't guarantee some other subtype of `Base` doesn't exist
} If //////////////////
// model.ts
export sealed abstract class Base {
...
}
export class Foo extends Base {
...
}
export class Bar extends Base {
...
}
...
//////////////////
// model2.ts
export class Baz extends Base { // Error: Baz cannot extend Base as Base is sealed.
...
}
//////////////////
// index.ts
const val = Base.factory();
if (val.isFoo()) {
console.log(val.a); // Can access `a` because `val` is now Foo
} else {
console.log(val.b); // Succeeds because the compiler knows that if `val` isn't `Foo`, it must be `Bar`
} Is there a way to accomplish this with just using a type union? Sure. //////////////////
// model-utility.ts
export type BaseType = Foo | Bar;
export function isFoo(obj: BaseType): obj is Foo {
return obj instanceof Foo
}
export function isBar(obj: BaseType): object is Bar {
return obj instanceof Bar;
}
//////////////////
// index.ts
const val = Base.factory() as BaseType;
if (isFoo(val)) {
console.log(val.a);
} else {
console.log(val.b);
} But that approach has several issues for me:
Interestingly, I can combine the two approaches: //////////////////
// model.ts
export type BaseType = Foo | Bar;
export abstract class Base {
static factory(): BaseType { // Return type is the type union instead of the abstract class
// Some logic to return either a Foo or a Bar
}
abstract isFoo(): this is Foo;
abstract isBar(): this is Bar;
}
export class Foo extends Base {
constructor(public a: string) { super(); }
override isFoo(): this is Foo { return true; }
override isBar(): this is Bar { return false; }
}
export class Bar extends Base {
constructor(public b: number) { super(); }
override isFoo(): this is Foo { return false; }
override isBar(): this is Bar { return true; }
}
//////////////////
// index.ts
const val = Base.factory(); // `val` is now of type `BaseType` (i.e. `Foo | Bar`) instead of `Base`
if (val.isFoo()) {
console.log(val.a);
} else {
console.log(val.b);
} Syntactically, this method works, but it has its own issues:
|
|
I have a extensible database design (I make plug-ins for my own project) the system plug-ins shall not be extended, so they would be final. |
It's bonkers that TypeScript still doesn't have I would use |
Every so often I think of this issue, and #33446 (final methods), and the 2016 and 2019 responses from the TypeScript team, and it brings me down, not so much for the rejection of the feature as for being inaccurate or illogical in the reasons given. I wrote a wordy comment in 2021, but I just want to push back more clearly before I give it a rest for another few years. (1) If you are reading this, you probably know that the primary purpose of
In summary, Not a way to "request a low-level runtime behavior" that happens to "imply a type meaning," as Ryan argued. (Note that final fields are a different story; the point here is about final classes and methods.) (2) When asked if, while considering the pros and cons of final methods and final classes, we could discuss them as separate features, Ryan replied:
However, they are separate features, not essentially the same, because:
(3) Ryan also wrote:
It's hard to see how any set of modifier keywords in any language could be said to "completely solve" the "extremely complex problem" of the "interactions between a base class and its derived classes." As others have pointed out, by this standard, modifiers like (In Dart, by the way, Runtime enforcement should not be a major consideration, IMHO. The point of features like this is to aid programmers working together and reduce bugs. I realize there is the relationship with the evolution of ECMAScript/TC39 to consider, but it's so ironic, considering that TypeScript is all about adding static guarantees to your code that are utterly unchecked at runtime. (I know I keep bringing up Dart, but it's just a really interesting language to compare. Did you know that in Dart, operators like I sort of understand the argument that class features are the domain of ECMAScript, while type features are the domain of TypeScript, but... we have Update: TC39 discussion / Suggestion to use decorators!I just found this: TC39 Discourse: Final classes The current thinking seems to be, final classes (and methods) can now probably be implemented with decorators. They'll see if that takes off. I would argue this puts the ball in TypeScript's court as far as how to check these decorators statically. Or, why not have a compiler flag where the "final" keyword adds a decorator? Or, to save work, don't bother emitting decorators in the first version; see if people are dissatisfied with just static enforcement. I just want squiggles in my IDE. IMHO, it doesn't make sense for TypeScript to wait for TC39 to consider adding a |
Decorators kinda solve the problem, but not fully, already made a class decorator (definition, how it works), not perfect though, but gets the job done. @Final
class Foo<T> {
foo: T;
constructor(foo: T) {
this.foo = foo;
}
someFoo(): T {
return this.foo;
}
}
class SubFoo extends Foo<string> {
constructor(foo: string) {
super(foo);
}
}
const _ = new SubFoo('subbedFoo'); This will cause a runtime TypeError: Cannot inherit from the final class at Foo ... at SubFoo ... (The error message is not perfect yet). final class Foo<T> {
foo: T;
constructor(foo: T) {
this.foo = foo;
}
someFoo(): T {
return this.foo;
}
}
class SubFoo extends Foo<string> {} // You can't even do this, TS will complain
Same for methods. |
I would also like to have the I don't know how the rest feels about this, but I find myself wanting to use the missing feature in around 80% of the projects I participate.
Hey @mhegazy, based on the feedback in this issue got over the years, it looks like there is a lot of people that would be happy having it. Isn't worth considering removing the won't fix label and engage in discussions again? |
Any update ? |
We've been ignored, my friend. |
This comment, and all the argumentation preceding it about it being "extremely uncommon" to inappropriately override classes or methods, runs contrary to the well studied problem of the Fragile Base Class problem. It's ironic that in this post the fragility of classes was cited as an argument against introducing the I'm not suggesting TypeScript make classes and methods final by default and introduce something like an Any time someone argues, "just add a comment", they're fundamentally misunderstanding the point of static typing. That argument can be made enough to take us all the way back to assembly language. After all, you can just add comments indicating that a memory address holds a specific variable with a human readable name. The reason we introduce tools for this is to automate it. Responding to, "can we add Furthermore, the literature on the Fragile Base Class problem, which includes published research papers, thoroughly refutes the claim that the mistake is "extremely uncommon". It is, rather, so common that it has been christened with a proper name and made the subject of rigorous research. The comment, "I have never tried to subclass something only to find out it's final" is an ironic justification for it not being possible to mark something as final (that would explain why that guard has never been encountered). The reason for never encountering this is either because it's not supported in the language, or it's supported but not used appropriately by class authors. The correct question to ask is, "have I ever successfully subclassed something I was allowed to, only to find out later I shouldn't have?" The answer, "I have tons of OOP experience and that's never happened" just suggests a failure to properly identify the mistake or do root cause analysis to tie bugs back to it. Essentially every large OOP codebase I've worked on is rife with deep inheritance hierarchies that significantly abuse subclassing and overriding. TypeScript should support |
@aetherealtech The reason I'm not very empathetic toward the Fragile Base Class problem is that it's one of those problems that is almost entirely self-inflicted. To inherit from a class that wasn't intended to be inherited and to override behavior of that class is not an action to be taken likely, and if you've ended up breaking the class in the process, it is entirely your fault. If this is a regular problem for you, then maybe it's your development methodology that needs changing rather than all programming languages. While it's a good idea to design a language in a way that prevents programmers from shooting themselves in the foot, it's less of a priority when its with a gun they probably shouldn't have been holding in the first place. If this was a ticket about adding And for the record, the difference between this and other things that separate us from assembly (like, say, null safety or an automatic garbage collector) is that those things help prevent mistakes that are incredibly easy to make even when you're being careful and trying to follow best practices. |
Over 60% of all software engineers in the world have 5 years or less of programming experience, and they don't know better. (source: https://www.developernation.net/developer-reports/dn26/) "You screwed up and it's your fault," isn't a good reason to not make final classes available. Many of us are writing classes that other (junior) software engineers will use and maintain later, and we'd like to help prevent them from screwing up. It's not their fault that they don't know what they don't know. |
@paulshryock No, you're right, it's not their fault. It's yours. It's the fault of their teachers, professors, seniors, peers, and mentors for not teaching them good conventions before ushering them into the professional world. When a junior programmer overrides a class that they shouldn't have overridden and it ends up breaking everything, who is that on, exactly? This isn't something like which architecture to use or which testing methodology is better. This is up there with "don't use gotos/singletons/global variables without a damn good reason" that every professional developer should know regardless of level of expertise. You are demanding that programming languages implement features that ultimately only serve to make up for the failings of education and professional support systems. What's next? Do we implement a language feature that makes it impossible for junior programmers to waste their time with micro-optimizations and convoluted one-liners? This isn't merely a case of "you screwed up and it's your fault", because that can be said of just about every language feature that gets misused. This is a case of "you screwed up and it should be immediately obvious how you screwed up without the language needing to tell you that you screwed up". You wouldn't advocate that a carpenter pad their hammers on the off chance their apprentices might start smashing their tables in half. And here's another reason why I don't like this feature as just a simple The only reason to support |
I agree with a previous poster that what concerns me is not the decision to turn down this feature request but the illogical reasoning being used to justify that decision. I'm actually surprised that there seems to be (unanimous?) agreement that subclassing/overriding is usually incorrect, and whether it is or even can be correct in a given circumstance is determined by the implementation of the class being overridden (the base class) rather than the proposed override... but that these points are being submitted as justification for not giving class authors the ability to control and forbid overriding their classes. To help clarify, what I would expect to see as a counterargument against this feature is an argument that forbidding overrides is needlessly restrictive and incorrectly anticipates a lack of possible, valid, unforeseen by the base class author, overrides. If that were the case, But everyone seems to be in agreement that isn't true. The class author is the one who knows whether a valid override is possible, and is responsible for implementing the base class in a way that allows it. In that case, isn't For example, Stroustrup argues that you can't ever be sure that no one should want to subclass your class. A subclass can add additional fields that will pass through code that works with the class transparently. It's a purely additive change. It's rather method overrides that can cause trouble, so in C++ you can't close off a class to being extended but you must opt into subclasses being allowed to override methods (using If you agree with his reasoning, you would reasonably oppose supporting But I'm not hearing any arguments like that. I'm just hearing arguments about how it's so wrong to freely subclass and override that it would somehow insufficiently signal how wrong it is to allow the language to forbid it. It's an argument I've heard before (like from C developers opposed to smart pointers because developers should be trained to do manual memory management better) that's never made sense to me. It's not a matter of not understanding the problem but standard human forgetfulness and tedium. Just the other day I refactored a TypeScript design from a base If I decide it's correct to subclass a third party class, but then on an update to that dependency the base class design changed and this override no longer works, I'd like to be alerted to that as soon as I upgrade the dependency, and not have a chore to go read and become an expert in the class's private implementation every time I upgrade the dependency. The author can signal this update by adding I also find it surprising the, "just don't use it incorrectly" argument is appearing in the context of TypeScript. This is exactly what TypeScript is for! In JavaScript to call a function correctly you have to go study the implementation of it to know how many and what type of arguments to pass in, and what you're going to get back. And anyone who calls it has to restudy it every time it changes. TypeScript renders this situation (which I consider to be insane, possibly from bias working in statically typed languages) sane by adding a typed signature to a function's interface, the public part that callers need to be familiar with, precisely so they don't have to go study the implementation repeatedly to know how to use it correctly. You seem resigned to the inevitability that classes are just fragile, that's the way it is and has to be. But that is because there's no way for the author to control overrides. If the language supports that, and class authors are trained to use it appropriately (anything not marked I don't want to oversell it. TypeScript has a huge ecosystem of classes, some very common and built deeply into foundation APIs like browsers or Node, and sealing off most of them (or most of their methods) with But it both enables a way to prevent the problem from getting worse, and at least provides the tools to incrementally patch those holes over time. Without this language feature, TypeScript classes are forever doomed to being fragile. The senior developer who knows his stuff will probably, correctly, deal with this by for all intents and purposes banning subclassing/overriding in his code (at least of third party classes). But this isn't good either, because that closes off opportunities to correctly and safely use those language capabilities. I don't see how this is an issue of developer training. The unfettered ability to incorrectly override certainly isn't supplying the training the professors dropped the ball on, it's rather letting the untrained developers crash their cars on the highway we all drive on, and forcing the properly trained developers to drive ultra slowly (better than recklessly fast but worse than a sensible speed limit) because they know the highway is unsafe. Also, wouldn't an untrained developer having the idea to use inheritance to solve a problem, then being blocked from doing so because the class/method he wants to override is |
Hand-wringing against Dart 3 last year introduced class modifiers including keywords such as
Dart previously had fewer distinctions than TypeScript, not distinguishing between an interface, a class, and an abstract class (IIRC). There's no objective grounds to argue against such niceties, even if each language makes different decisions about what to support. |
I see now the argument was submitted against But I think this is misunderstanding the correct use of An override risks breaking a class's invariants. For an override to not break an invariant the base class's implementation needs to follow rules to ensure it doesn't inappropriately rely on behavior an override could change. Thus, a class author is always faced with a decision: invest the time to ensure a class's invariants can't be broken by overrides, and allow those overrides, or remove the need to make this investment (which competes with other priorities) by forbidding overrides. The decision isn't just a matter of the class author having a potential valid override in mind, but rather whether it's worthwhile to ensure an override is safe, or decide the risk of broken invariants is small enough to offer the class with "no warranty" so to speak, or at least a warning that overriding a method hasn't been carefully considered/tested and to do it at your own risk. On the matter of a class author perhaps incorrectly adding The wrapping class isn't recognized by code written to use the sealed class, so the underlying instance has to be unwrapped and passed into this code, which means that code can't use the modifications. If this is really necessary, some of that code that uses the class might also need to be wrapped with similar patterns. There is a lot of boilerplate to this (although JavaScript is dynamic enough it probably doesn't need to be manually typed out) but you shouldn't have to reimplement anything. Of course it's possible for class authors to misuse The worst risk is that passing a subclass with modified methods to that code works accidentally today, and then breaks on an update to that code (not your subclass). This is basically as bad as undefined behavior. The error will occur in a place that might not signal what the problem is, and you can't really protect against the risk except, ironically, by refusing to use a subclass at all. I will show my hand: my bias is strongly in favor of the latter. I write code that prioritizes making it impossible to write bugs over making it easier to finish a task. I know some developers disagree with this and reply with things like, "just don't write bugs" or "just test your code" (but I am, see this compiler safety is the test!). I can evangelize my perspective here but I think it's more important just to clarify that's the tradeoff, and it's why I favor (as I said at the beginning) everything being |
Anyone not living under a rock knows that most other programming languages have already changed. Modern langs are final-by-default, and often have multiple
This sounds like a |
☝ This. I know a lot of engineers and developers like to make fun of PHP as being behind the times, but even PHP has had final classes since 2004. It's bonkers that in the year of our Beyonce, 2024, TypeScript still doesn't have final classes. And the reason is... checking my notes... senior engineers are supposed to teach junior engineers about the pitfalls of class extension? Nice. So next time my company brings in new contractors to the team, we'll all drop everything, miss our deadlines, and teach them about programming for a few weeks. Meanwhile, the rest of us would like to just get work done and use decent language features like final classes. 🤷 |
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:
The text was updated successfully, but these errors were encountered: