-
Notifications
You must be signed in to change notification settings - Fork 12.4k
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. |
I put a little more work into this decorator to make it work. It does provide runtime enforcement that a subclassed This gist provides more complete code (tests, etc), but here's the key part: export declare interface Type<T> extends Function {
new (...args: any[]): T;
}
function preventSubclassing<T>(constructor: Type<T>): Type<T> {
const newCtor = function (...args: any[]): T {
if (new.target.prototype !== constructor.prototype) {
throw new TypeError(`${constructor.name} cannot be subclassed`);
}
return new constructor(args);
};
// copy prototype so instanceof operator still works
newCtor.prototype = constructor.prototype;
return newCtor as unknown as Type<T>;
}
/**
* Class decorator that prevents type modification and extension. If the decorated
* class is subclassed, a `TypeError` is thrown when the subclass is constructed.
*
* The name `@final` is used instead of `@sealed` b/c in Javascript, sealing
* protects against object modification (not type extension).
*/
export function final<T>(constructor: Type<T>): Type<T> {
const newCtor = preventSubclassing(constructor);
Object.seal(newCtor);
Object.freeze(newCtor.prototype);
return newCtor;
} Usage: @final
class A {}
class B extends A { }
const b = new B(); // throws TypeError('A cannot be subclassed'); I'm not certain that it's production ready yet - consider it a concept. For example, right now there are 2 calls to I would still love to see first-class typescript support for a |
Notably absent from this discussion is that final classes and final methods (independently) allow TS to analyze assignments in methods called in the constructor as if they were written directly in the constructor. With class SomeClass {
someProperty: number;
constructor() {
this.setup();
}
// or init()
setup() {
this.someProperty = 0;
}
} despite the fact that the following compiles without error: class SomeClass {
someProperty: number;
constructor() {
this.someProperty = 0;
}
} The reason is that TS doesn't know whether any subclass of The workarounds, which don't provide assurance that
I'm sure most of us see all of these as both bad practice and difficult to maintain. Adding the I suppose many developers overlook that methods called in the constructor can be overridden, and rely on behavior defined in the "base class." In particular, in the above example, |
@mhegazy There is no question this keyword makes sense and is useful both on class and methdo level - and I do not talk about runtime optimization here. I suggest you put this on the roadmap along with a bunch of other heavily upvoted features. |
How you see the problem (based on replies)
Another aspect: OOP design I want to cite an chapter title from Joshua Bloch's Effective Java:
This chapter builds on the well known open-closed principle which is supported by TypeScript. If you write a class and you wan't your users to build on it you allow them to extend the class. Why? Because then it will be useful in countless ways! You want to prohibit extending the class as in actuality inheritance can break invariants of your base class. This is not a theoretical issue. Real-world example: Angular |
Hi. I respect the comment in #9264 (comment) that this request isn't going to be relitigated. I scanned, but did not read all the cases, so apologies if this use case has already been described. I have this set-up:
There are somewhere between 300-400 subclasses of There is also a controller of sorts which does a lot of the things that should be in a common
If Edited to add I acknowledge this is solvable other ways, like renaming all the |
Consider to reopen. Prevent methods from override with the class MyService {
public getConfig() {
// Override me.
}
public start() {
const config = this.getConfig();
// Get config and do stuff. But don't override me. Needs a keyword to prevent from overriding.
}
} https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/sealed |
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. |
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: