Skip to content
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

Closed
Zorgatone opened this issue Apr 26, 2016 · 168 comments
Closed

Support final classes (non-subclassable) #8306

Zorgatone opened this issue Apr 26, 2016 · 168 comments
Labels
Suggestion An idea for TypeScript Won't Fix The severity and priority of this issue do not warrant the time or complexity needed to fix it

Comments

@Zorgatone
Copy link

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
Copy link
Contributor

mhegazy commented Apr 26, 2016

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

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Apr 26, 2016
@Zorgatone
Copy link
Author

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
Copy link
Contributor

mhegazy commented Apr 26, 2016

This is a new feature, will be released in TS 2.0, but you can try it using typescript@next. see #6885 for more details.

@Zorgatone
Copy link
Author

Ok thank you

@duanyao
Copy link

duanyao commented Apr 27, 2016

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

@mhegazy
Copy link
Contributor

mhegazy commented May 17, 2016

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.

@mhegazy mhegazy closed this as completed May 17, 2016
@mhegazy mhegazy added Won't Fix The severity and priority of this issue do not warrant the time or complexity needed to fix it and removed In Discussion Not yet reached consensus labels May 17, 2016
@0815fox
Copy link

0815fox commented Jun 20, 2016

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
Copy link

0815fox commented Jun 20, 2016

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
Copy link
Contributor

mhegazy commented Jun 20, 2016

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
Copy link

mindarelus commented Aug 7, 2016

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
Copy link

mcdirmid commented Sep 9, 2016

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
Copy link

pauldraper commented Oct 10, 2016

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
Copy link

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

@aluanhaddad
Copy link
Contributor

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
Copy link

0815fox commented Oct 24, 2016

@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
Copy link

hk0i commented Oct 29, 2016

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
Copy link

rylphs commented Nov 1, 2016

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
Copy link

@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
Copy link

MrDesjardins commented Mar 30, 2017

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.

@mahdi-farnia
Copy link

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.

@cbutterfield
Copy link

cbutterfield commented Sep 9, 2022 via email

@kayahr
Copy link

kayahr commented Sep 10, 2022

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.

@binki
Copy link

binki commented Sep 10, 2022

@mahdi-farnia

What does typescript exactly do in runtime for optimizing??????

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.

@cbutterfield

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 wouldn’t say this so strongly, but I do agree. TypeScript lead the way with JavaScript language innovation by introducing an implementation of async/await whose syntax was adopted into ECMAScript (even if nuances in behavior are not identical). It even started out with providing class before JavaScript had it. But TypeScript decided that they want to avoid creating features which could force them to deviate from future developments in ECMAScript. They have gone so far with this, that they are no longer willing to innovate or deviate from pure JavaScript behavior even for things which exist entirely in TypeScript’s realm such as nominal or final classes.

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…

@Max10240
Copy link

Max10240 commented Jul 15, 2023

Does this help? (At the method or attribute level)
TS playground link

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() {
  }
}

@binki
Copy link

binki commented Jul 15, 2023

@Max10240 doesn't that break external module doing new A().baz = 'asdf';?

@Max10240
Copy link

@Max10240 doesn't that break external module doing new A().baz = 'asdf';?

Yes. The above answers only apply to the method or attribute level. If you want to make a property to be final , I think it must first be "readonly". I updated the comment to explicitly add the "readonly" modifier!

@pauldraper
Copy link

pauldraper commented Jul 25, 2023

TypeScript lead the way with JavaScript language innovation by introducing an implementation of async/await whose syntax was adopted into ECMAScript (even if nuances in behavior are not identical).

The ES proposal predated TypeScript implementation by over a year and a half.

It even started out with providing class before JavaScript had it.

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.

@Voltra
Copy link

Voltra commented Oct 22, 2023

Sometimes TypeScript design decisions surprise me. How can you both have override but not final? Every reason you have for override can be adapted for final/sealed.

Kinda the same vibes as "not generating anything at runtime" but also having had both the decorators and int-backed enums generate extra code.

@EduApps-CDG

This comment was marked as off-topic.

@Abion47
Copy link

Abion47 commented Feb 28, 2024

I ran into an issue stemming from the lack of sealed that I think introduces a use-case that hasn't yet been covered in this thread. (If it has, I apologize, but it's a very long thread.)

Some languages are coming out with the final/sealed feature but with a twist: the class can be inherited but only from the same file/module that the base class was defined. This extends the idea of an unmodifiable class and adds the idea of a closed enumeration of subclasses, which I think extends to TypeScript nicely as it allows subclasses to be defined in a way that mimics a type union.

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 val isn't a Foo, it must be a Bar. This is something you can do with a type union, but not with subclasses.

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 Base was marked as sealed, however, the compiler would be able to know that Foo and Bar are the only subtypes of Base that can exist and inform the type system accordingly:

//////////////////
// 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:

  1. It's idiomatic of an FP-oriented coding style, which is a stark contrast to both my personal coding style and the general coding style of my team at work. (It is what it is.)
  2. It requires additional boilerplate to be written especially if the base types I'm working with come from a library I don't control.
  3. The isFoo and isBar utility functions are separated from the Base/Foo/Bar family of classes which hurts API discoverability.
  4. It's yet another thing I have to import which unnecessarily bloats the import section.

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:

  1. This change affects the entire API as everything that returns Base now has to return BaseType instead. Otherwise, the "solution" won't work.
  2. Conceptually, it just feels wrong that the type of val is now Foo | Bar instead of Base. If I'm using OOP, I would prefer to stay in the realm of OOP.

@Voltra
Copy link

Voltra commented Feb 28, 2024

final in the cases of C++ or Java is used for compile-time optimization sure, but it's also used to statically enforce a "contract" for library consumers: you cannot override (or extend for classes' case).

@EduApps-CDG
Copy link

but it's also used to statically enforce a "contract"
@Voltra we learn that in Software Engineering class.

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.

@paulshryock
Copy link

paulshryock commented Apr 27, 2024

It's bonkers that TypeScript still doesn't have final classes and methods.

I would use final in every project, if it was available.

@dgreensp
Copy link

dgreensp commented Apr 27, 2024

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 final in Java is not as a performance hint, but for contract enforcement (as taught in intro software engineering, etc). What you may not know (or may misremember) is, it's not a secondary purpose of final, either! Using final to try to speed up methods or classes has been discouraged, and not worked, since the Java HotSpot compiler came out in 1999. Here is a column from October 2002 debunking the myth that one should consider using final for performance reasons (referenced by a 2003 column titled Urban performance legends):

Like many myths about Java performance, the erroneous belief that declaring classes or methods as final results in better performance is widely held but rarely examined. The argument goes that declaring a method or class as final means that the compiler can inline method calls more aggressively, because it knows that at run time this is definitely the version of the method that's going to be called. But this is simply not true. Just because class X is compiled against final class Y doesn't mean that the same version of class Y will be loaded at run time. So the compiler cannot inline such cross-class method calls safely, final or not. Only if a method is private can the compiler inline it freely, and in that case, the final keyword would be redundant.

On the other hand, the run-time environment and JIT compiler have more information about what classes are actually loaded, and can make much better optimization decisions than the compiler can. If the run-time environment knows that no classes are loaded that extend Y, then it can safely inline calls to methods of Y, regardless of whether Y is final (as long as it can invalidate such JIT-compiled code if a subclass of Y is later loaded). So the reality is that while final might be a useful hint to a dumb run-time optimizer that doesn't perform any global dependency analysis, its use doesn't actually enable very many compile-time optimizations, and is not needed by a smart JIT to perform run-time optimizations.

In summary, final: Not a performance thing, at all.

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:

I don't see any lack of duality between final classes and final methods; the latter is simply a more fine-grained control of the former and the motivations are largely the same.

However, they are separate features, not essentially the same, because:

  • They do different things.
  • If I can't have one, for some technical or moral reason, I still want the other.
  • It's a lot easier to make sure people don't subclass a class than to make sure they don't override a method, such as by accident or after a refactoring, just because it's a harder mistake to make in the first place.
  • Enforcement-wise, it's a lot easier to work around lack of final classes, e.g. with private constructors or—I haven't used this, but I imagine—a runtime check like this.constructor === MyClass.
  • Google's Dart language (as a point of reference) addresses them in two different ways, a final class modifier and a @nonVirtual method annotation.

(3)

Ryan also wrote:

The general consensus on final classes was that from a type perspective, interactions between a base class and its derived classes are a complex interplay that can't be accurately described by providing a few keywords. By that measure, final of any form would be a "half measure" that doesn't really completely solve what is really an extremely complex problem.

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 override and protected would be condemned as "half measures" and not implemented. It's a heavy-handed argument that is hard to take at face value.

(In Dart, by the way, @override and @protected are annotations—similar to decorators—not keywords, and I'm in a thread right now about what it would take to make protected a language keyword, which would be slightly less clunky in certain ways. Part of the complexity being raised is that any core language feature, for philosophical reasons, would have to be implemented in the runtime, too, with runtime checks. There isn't the same split of TypeScript and TC39 being different parties, and the Dart team has expressed low-priority willingness to eventually bring protected into the language, maybe with some modifications, but there is a similar sense of "you can't have nice static things because that sounds like something the runtime could get in on." Different languages seem to have different versions of this. Anyway, it could be worse, TypeScript could be refusing to implement protected, or the definitely-wouldn't-be-added-today private.)

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 ! and as emit runtime checks?? There is a concept of "runtime soundness," where if there are holes in the static type system, they must be plugged at runtime. This is partly because Dart doesn't just compile to JavaScript, it can be AOT-compiled to machine code. It is very hard in Dart for your static types and your runtime types to differ at all—even type parameters are reified at runtime—while in TypeScript, it is super easy; you can set up a static fantasy land if you want, and it will never be checked. So that's part of why it's ironic.)

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 override. And TC39 doesn't seem to be working on final? TypeScript implementing something here might even lead the way as far as TC39.

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 final keyword to the language, when TC39 only needs to be concerned with runtime enforcement, and they are waiting to see if decorators solve that problem. At the end of the day, there's no uncertainty about what the semantics of final would be, anyway, I don't think, so no downside in TypeScript moving forward, whether it's a keyword or a decorator.

@AshGw
Copy link

AshGw commented May 4, 2024

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).
Wouldn't it be better to error out at compile time ?

  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.

@princess-emma
Copy link

I would also like to have the final keyword added to the language.
It can be mostly done using decorators now, but at the cost of runtime errors. Wasn't typescript supposed to protect us from them by having a compilation step?
Additionally, I don't see how this can be complicated to add, since it's not that different from private and such.

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.

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.

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?

@Ayfri
Copy link

Ayfri commented Sep 17, 2024

Any update ?

@princess-emma
Copy link

Any update ?

We've been ignored, my friend.

@aetherealtech
Copy link

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

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 final keyword. This is a perfect inversion of the truth. Fragility of classes is exacerbated, if not fundamentally caused by, the fact that final is not the default behavior, and (if final is supported at all) must be opted out of. The alternative, which is supposed to help fix the fragility of classes, is to require a keyword for non-final classes or methods, such as C# requiring a method to be marked virtual in order to be overridable (C# makes overridability the default for classes however).

I'm not suggesting TypeScript make classes and methods final by default and introduce something like an overridable keyword, only because that's a breaking change (it's just too late to consider that, for better or worse). I bring it up to point out that the result of careful study of the fragility of classes has produced the exact opposite conclusion argued here, that finality should not only be an option but the default option. Since it's too late to make it the default, at least we should make it an option.

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 final" with, "just don't subclass it then", is responding to, "can we put a guard in for this mistake" with, "just don't make the mistake". Putting in a guard is how we "just don't make" the mistake! We automate things because manual processes are tedious and error prone. In the context of TypeScript, "just add a comment" is essentially saying, "just write JavaScript".

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 final on both classes and methods so that TypeScript developers can then start adopting the practice of marking every class and method final by default (except where redundant) and removing final only when it becomes necessary. If that practice gets widely adopted, then maybe classes will stop being so fragile to begin with. This is one of the most important lessons the OOP community learned the hard way from decades of hard empirical evidence, and it would be a shame to have to repeat that history.

@Abion47
Copy link

Abion47 commented Nov 5, 2024

@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 goto support, we probably wouldn't even be having this conversation for precisely the same reason.

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.

@paulshryock
Copy link

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.

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.

@Abion47
Copy link

Abion47 commented Nov 6, 2024

@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 final marker. Because sometimes developers do know what they are doing, and sometimes they do have a good reason to subclass that wasn't intended to be subclassed. final prevents them from being able to do that, forcing them to reimplement the entire system from scratch in order to do what they want. And making final the default behavior is even worse because it puts the burden on the library maintainer to anticipate what should be subclassable which is an exercise in futility. All this feature is doing is catering to the lowest common denominator at the expense of people on the other end of the spectrum.

The only reason to support final as a feature is if we wanted to force programmers to develop with certain paradigms, and that flies in the face of the modern multi-paradigm world. Give your juniors a toolbag and teach them what the tools do, then let them make their own mistakes. Don't give them a hammer and tell them to only concern themselves with the nails.

@aetherealtech
Copy link

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, final would be giving class authors a power they really shouldn't have. It would either be never correct, or correct in only such a minority of situations to not be significant, to close off overriding, because there's no way for a class author to know if anyone might have a valid use case for an override.

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 final the language feature that expresses this opinion on the matter?

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 virtual) or access non-public members (using protected).

If you agree with his reasoning, you would reasonably oppose supporting final on classes (I get his point and agree final on methods is much more important, I also don't have an issue with forbidding subclasses entirely though, especially in languages with a proper interface concept).

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 type to an base abstract class with one abstract member and a method that uses that abstract member but shouldn't be overriden itself. The old design was implementing that method in each concrete class, so those became overrides after the refactor and needed to be deleted. It would have been nice for the compiler to throw an error if I overlooked one. It's not an issue of me being naive enough to think the method should be overridden. I know it shouldn't, and I'd like the tooling to help me enforce that and bring my attention to anywhere it's being violated.

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 final which will break my override and force me to fix it.

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 final must be freely overridable without breaking the base class's invariants), then classes won't be fragile anymore! Isn't this better than chastising developers for not treading lightly enough atop unfixably fragile classes?

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 final is a breaking change that each class author would have to opt into, which could render large amounts of dependent code broken (but revealing by doing so that all that code is really brittle and risks accidentally breaking on a dependency update anyways). Alternatively, it at least encourages them to decide that if they're not going to add final, they must design the class to allow overrides. Simply adding the keyword to the language won't suddenly fix the fragility of classes in the TypeScript ecosystem.

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 final, be an effective way to provide this training? Wouldn't it force the very conversation the untrained developer needs in a real world, non-academic context that makes it meaningful and memorable?

@dgreensp
Copy link

dgreensp commented Nov 6, 2024

Hand-wringing against final as part of professional software development seems exceedingly silly to me.

Dart 3 last year introduced class modifiers including keywords such as final, base, and sealed (sealed classes can only be extended in the file in which they are declared, which also benefits exhaustiveness checking in pattern matching; note Dart doesn't have type unions so using classes is the only way to do a lot of things... anyway, not to distract from the larger point). Here's how they announced it:

A third Dart 3 language feature is class modifiers. Unlike records & patterns that we expect every Dart developer to use, this is more of a power-user feature. It addresses the needs of Dart developers crafting large API surfaces or building enterprise-class apps.

Class modifiers enable API authors to support only a specific set of capabilities. The defaults remain unchanged though. We want Dart to remain simple and approachable. So, like before, regular classes can be constructed, extended and implemented, as shown in the following examples:

...

  • With a final class, you can close the type hierarchy preventing any subclasses outside of your own library. As a sample benefit, this allows the API owner to add new members without risking breaking changes to the consumers of the API.

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.

@aetherealtech
Copy link

aetherealtech commented Nov 6, 2024

I see now the argument was submitted against final that the class author shouldn't be able to forbid all overrides because he can't anticipate a possible valid one and would be needlessly closing that off by marking things as final.

But I think this is misunderstanding the correct use of final, and also favoring easiness over correctness and especially stability through modification.

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 final forcing a client to reimplement the class's behavior, that's generally not necessary. There are various design patterns like Decorator or Proxy that work by wrapping a class instance in another class that forwards methods to the wrapped instance, and has an opportunity to intercept and modify them. This is actually more flexible than subclassing because the "base" class can be assigned, and even changed at runtime.

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 final and render this wrapping technique more pervasive than it should be, but developers can misuse every feature, and also it's important that all that code using the class as is might not have been designed to work with a modified version of the class. What is more important: making it easier to solve a problem correctly (i.e. no wrapping boilerplate), or making it harder to solve it incorrectly (not letting you just pass a modified subclass directly into code not designed with that in mind)?

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 final until an explicit decision for it not to be. I'm choosing safety and compile time debugging over speed, under the assumption that overriding is unsafe until explicitly considered and designed to be safe.

@danielzgtg
Copy link

maybe it's your development methodology that needs changing rather than all programming languages.

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 final keywords. Legacy langs have a final keyword. Ancient langs don't.

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

This sounds like a C++/assembly JavaScript "real programmer", not a TypeScript programmer. People who think final and other type safety features are for wimps can always go back to using JavaScript. They just shouldn't hold back this feature at the expense of the rest of us.

@paulshryock
Copy link

paulshryock commented Nov 10, 2024

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 final keywords. Legacy langs have a final keyword. Ancient langs don't.

☝ 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. 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Suggestion An idea for TypeScript Won't Fix The severity and priority of this issue do not warrant the time or complexity needed to fix it
Projects
None yet
Development

No branches or pull requests