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

Prevent class or its methods from overriding. With keyword like final or sealed. #50532

Closed
3 of 5 tasks
infacto opened this issue Aug 30, 2022 · 20 comments
Closed
3 of 5 tasks
Labels
Duplicate An existing issue was already created

Comments

@infacto
Copy link

infacto commented Aug 30, 2022

Suggestion

πŸ” Search Terms

  • prevent class or its methods from overriding
  • sealed
  • final

βœ… Viability Checklist

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

There are situations when you want to avoid other classes overriding the entire class or just some methods or fields.
For these cases there are keywords in other languages like Java or C#. Called final or sealed / virtual, etc.
TypeScript could take inspiration from these keywords or find its own way. Which we want to find in this issue.

This way or keyword is for TypeScript only. There is no need for optimization on runtime or effect on generated code. It's a part of type definition and acts similar like keywords readonly, public, abstract, protected, etc. It's only about hints during development.

πŸ“ƒ Motivating Example

Let me show an example what brought me here:

class MyService {
  /** This method is intended for overriding. */
  public getConfig() {}

  /** This method should not be overridden. The method cannot be inherited from subclass. */
  public final start() {
    const config = this.getConfig();
    // Do stuff...
  }
}
/** The following class cannot be inherited. */
final class AnotherService {}
class SubService extends MyService {
  override getConfig() {
    return { value: 42 };
  }

  override start() {} // <- Error: You cannot override a final method.
}

class HookService extends AnotherService {} // <- Error: You cannot override a final class.

Or using the C# variant using keywords like sealed. More examples are welcome.

πŸ’» Use Cases

In some cases you want to control which classes or methods can be overridden by derived classes. For example, you have a class with configuration methods and you don't want active methods to be able to be overridden. It's also relevant for defintion files d.ts in APIs.

We could also check out the uses cases from other language why these keywords exists. Even if Java and/or C# uses the final class to optimize the class at runtime. TypeScript is at the type level and only acts during development to prevent overwriting like similar related features. ...

For more information, see the links below:

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/sealed

When applied to a class, the sealed modifier prevents other classes from inheriting from it. ... You can also use the sealed modifier on a method or property that overrides a virtual method or property in a base class. This enables you to allow classes to derive from your class and prevent them from overriding specific virtual methods or properties.

https://docs.oracle.com/javase/tutorial/java/IandI/final.html

You can declare some or all of a class's methods final. You use the final keyword in a method declaration to indicate that the method cannot be overridden by subclasses.

Background: I know Java and C#. But I'm not a Java or C# developer. My information is based on research on web.

@whzx5byb
Copy link

whzx5byb commented Aug 30, 2022

Duplicate of #1534 and #8306

@infacto
Copy link
Author

infacto commented Aug 30, 2022

Yes, more or less. And now we have a well described issues which takes care about class and its methods and fields with examples and possible solutions. Btw. I only knew the closed issue #8306. Which was closed for incomprehensible reasons. See closing post.

@jonlepage
Copy link

it a 8 year feature request :)
no more hope

@eman2673
Copy link

eman2673 commented Sep 6, 2022

I expected this to exist only to find out that it didn't. Bit of a shame maintainers don't see value in this (my assumption based on the fact that it doesn't exist and similar issues have been closed without resolving).

@andrewbranch andrewbranch added the Duplicate An existing issue was already created label Sep 7, 2022
@mahdi-farnia
Copy link

Really handly feature.

I think better to work on this feature than playing with unknown type in various version of typescript. ( Enhancement are needed but not as much as MAIN features like this )

@kayahr
Copy link

kayahr commented Sep 10, 2022

I suggest removing the duplicate flag and instead mark #8306 as a duplicate of this issue here. 8306 was initially about final classes and was closed as "won't fix" for the wrong reasons (misunderstanding about runtime performance optimizations). This new issue here is about final methods AND classes, is described in great detail and makes clear that it is NOT about runtime optimizations. So please lets discuss about this very useful language improvement and do not blindly close this issue in favor of 8306.

Typescript already has abstract classes to define that the class' sole purpose is to be extended by other classes. The missing final keyword is some kind of the opposite. It can be used for classes at the end of the inheritance chain to make sure that no one further inherits from it. We need this control to write clean code. To define which classes MUST be extended, which classes CAN be extended and which classes MUST NOT be extended. Two of these three cases is supported by TypeScript, the third one is missing, so please consider implementing final classes.

The same goes for methods. We can define methods which MUST be implemented (abstract methods) and we can define methods which CAN be overridden but there is currently no way to define methods which CAN NOT be overridden.
So again final would be very useful. TypeScript even implemented the override keyword for methods so the compiler can detect methods which are intended to override a method but which no longer do this because the method in the base class was removed or renamed. This is very useful and the final keyword is playing in the same league to detect programming errors where methods are overridden which must not be overridden.

@a11delavar
Copy link

This should have been on the feature list from day 1, or at least when the explicit override became available.

@a11delavar
Copy link

@infacto I think the from keyword got there by accident (?)

Let me show an example what brought me here:
...

class SubService extends from MyService {
// ----------------------^^^^
}

@cbutterfield
Copy link

cbutterfield commented Sep 10, 2022

Final (or sealed) methods are a critical tool in writing utility classes that can safely and reliably extended. So I totally concur with this new attempt to get them added to Typescript.

@jonlepage
Copy link

Typescript already has abstract classes to define that the class' sole purpose is to be extended by other classes. The missing final keyword is some kind of the opposite. It can be used for classes at the end of the inheritance chain to make sure that no

ECS pattern is a great example of usage of final for classes components.
Extends a class component will just make crash the engine and the is no way to prevent this before compile actually!

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@infacto
Copy link
Author

infacto commented Sep 14, 2022

This is a sad reaction from the TypeScript team. I think we just want an understandable statement. It's okay if you say you don't want to do this for internal reasons or concerns. But instead we get confusing or dubious answers like this. It's just rude and not the truth. We just want a statement. Not answering is ignorant. You're probably just annoyed. I understand that when I look in the issue tab. But anything is more helpful than that. It deserves a few respectful and explanatory words. Nobody is forcing you to implement everything the community suggests. I think you are professionals and closing issues like this or the others must be a valid reason. But for what reason? We just want to understand.

@a11delavar
Copy link

The way this feature is handled disheartens me for almost a decade, and all this without a viable reason is at this point imo like shouting "we don't care, and neither should you".
Fair enough I guess.

@doronguttman
Copy link

This is very disappointing, especially when the official TypeScript documentation even gives this exact scenario as an example of use of class decorators. Only issue is, that the decorator solution will cause an error at runtime instead of dealing with it at design time.

#shiftright πŸ€¦β€β™‚οΈ

@obeobe
Copy link

obeobe commented Jan 8, 2023

Classic Microsoft, right?

@Max10240
Copy link

Max10240 commented Jul 15, 2023

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

declare const _: unique symbol;
type Forbidden = { [_]: typeof _; }
type NoOverride<T=void> = T & Forbidden;
function makeNoOverride<T>(value: T): NoOverride<T> {
  return value as any;
}

class A {
    readonly baz: NoOverride<string> = makeNoOverride('');

    foo(): NoOverride<{ a: string }> {
        return makeNoOverride({ a: '' });
    }

    // if this function return nothing, use `NoOverride` only
    bar(): NoOverride {
        console.log(0);
    }
}

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

cc @vddrift

@vddrift
Copy link

vddrift commented Aug 26, 2023

Works for me! Thanks @Max10240 ! I like to wrap it like this:

declare const _: unique symbol;
type Forbidden = { [_]: typeof _; }
type NoOverride<T=void> = T & Forbidden;

class A {
    readonly baz: NoOverride<string> = '' as any;

    // Note - `ReturnType & NoOverride`
    foo(): NoOverride<{ a: string }> {
        return { a: '' } as any; // OR:   as NoOverride<{ a: string }>
    }

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

@janryWang
Copy link

@andrewbranch
Can you explain why this feature is not supported? Is it because it conflicts with Javascript standards? Or is it because of compilation performance?

@vddrift
Copy link

vddrift commented Feb 28, 2024

[updated] Does this help? (At the method or attribute level) [TS playground link](https://www.typescriptlang.org/play?

Interesting approach. You could simplify it further by leaving out some more typescript:

Personally I like to separate concerns. So I prefer this, which is clearly Typescript

readonly baz: NoOverride = '' as any;

Over this, which is javascript used to fix a Typescript issue

readonly baz = makeNoOverride('');

Perhaps the following is the cleanest way to set a value baz and then declare it as NoOverride

declare const _: unique symbol;
type Forbidden = { [_]: typeof _; }
type NoOverride<T=void> = T & Forbidden;

class A {
    readonly baz = '' as NoOverride<string>;

    foo() {
        const result = { a: '' }
        return result as NoOverride<typeof result>;
    }

    // if this function return nothing, use `NoOverride` only
    bar(): NoOverride {
        console.log(0);
    }
}

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

Max10240 commented Feb 28, 2024

Interesting approach. You could simplify it further by leaving out some more typescript:

Yes, for the most part, like you, I prefer to let TS do its own type inference, like:

// prefer:
let s = '...';
const baz = makeNoOverride('');

// than: 
let s: string = '...';
const baz: NoOverride<string> = makeNoOverride('');

But for function return values, I prefer to declare the return type explicitly (so that the code reader can see it from the start).
In addition, a lot of "as xx" makes me "afraid" of this code (at the type level).

So the final version in my mind looks something like this:

declare const _: unique symbol;
type Forbidden = { [_]: typeof _; }
type NoOverride<T=void> = T & Forbidden;
function makeNoOverride<T>(value: T): NoOverride<T> {
  return value as any;
}

class A {
    readonly baz = makeNoOverride('');

    foo(): NoOverride<{ a: string }> {
        return makeNoOverride({ a: '' });
    }

    // if this function return nothing, use `NoOverride` only
    bar(): NoOverride {
        console.log(0);
    }
}

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

ps: Friendly communication as a personal code style only :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests