-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
[Proposal] Limit the compiler error about overriding the base class property with getter/setter accessors pair to the useDefineForClassFields : true
case only
#44927
Comments
Can you give examples of production libraries that use this pattern? One reason I was so confident in making it an error for all targets is that Microsoft, Google and Bloomberg checked their Typescript code with the error enabled and didn't run into any problems. Another reason, while I agree that [[Set]] is more expressive, is that the future is unfortunately [[Define]]. My thinking is that if nobody's code is broken now, we should keep it that way by preventing new code from being written that will need modification later. |
I've gotta say, it is very bad TC39 did that. Anyway, the OP has a good point: when
Note that the decorator would need to be applied inside the base class, which means one would still need to modify the external library where they import the base class from unfortunately. |
@sandersn We use this pattern in production at Bryntum. Sorry, can't provide a link to source code, since its proprietary. The use case is that we have a "regular" codebase, which provides data structures, like tree, and we have a "reactive" codebase. The "regular" codebase is mature and stable, maintains backward compatibility, making changes in it is really not an option. However we need to combine both codebases and make them interoperable. The example I've posted is actually a real-world case, which solves the integration task with the "property access augmentation" pattern. We also use this pattern extensively in our JavaScript codebase (which assumes ES5 and does not use native class fields). I also use this pattern a lot in my open-source libraries (this is why I'm advocating for easing the error message), for example here. |
One more example from my open-source project, testing tool called Siesta: There's a base class This pattern is very useful, for example, if I'd not use it in this "async assertion" case, I'd have to create a new method |
From a proprietary code base: @InterfaceType() // nestjs decorators
export class NotificationBase extends TimeStamps {
/**
* Whether the notification should be hidden in the app or not
* This is not an actual persisted field, it's overriden in subclasses instead
*/
@Field()
showInNotificationCenter?: boolean;
}
@ObjectType({ implements: NotificationBase })
export class TeamChatNotification extends NotificationBase {
// @ts-expect-error: https://github.com/microsoft/TypeScript/issues/44927
get showInNotificationCenter() {
return this.unreadMessageCount > 0;
}
} This is a common pattern we use. Since a decorator can only work on a regular field, there's no other way to define this sort of behavior. |
@andreialecu Shouldn't NotificationBase be abstract? |
@sandersn that wouldn't work in this case, no. Here's an example: @Module({
imports: [
TypegooseModule.forFeature([
{
typegooseClass: NotificationBase, // Cannot assign an abstract constructor type to a non-abstract constructor type.ts(2322)
discriminators: [...] There are many other scenarios where things would break if it was made abstract: However, I'm not sure what making it abstract would achieve since it doesn't prevent the TS error. The main reason for the pattern is to be able to define this in the base: @Field()
showInNotificationCenter?: boolean; Notice the decorator. It is used for a NestJS "code first" graphql schema definition. Since you cannot apply decorators on a plain getter, this is the only way to make it work that I can think of. In practice it all works properly, it's just the TS error that needs to be ignored. |
It seems like this'll cause compatability issues in the future? If a project with |
@webstrand The scenario you describe is valid, but its a matter of the community fragmentation, which will inevitably happen, based on the preferred semantic for class fields. Your concern is valid, however, how often do you extend a class from external library and you need to override the property from it with getter/setter pair? Its a matter of proper documentation for that library. In the same way, one could raise a concern, that what if some library is using native generators functions, but the project that is using it, has set the compilation target to |
The difference, as I see it, between this issue and generator/es5 is that I would be happier if this behavior were opt-out via a dedicated flag, rather than attaching more behavior to the It sounds like, going forwards, programmers will want a way to declare class properties with class foo {
declare foo = 5; // set semantics
bar = 5; // define semantics
} or something similar. And then the error would only need to be shown for fields with |
Hm.. Isn't it implicitly enabled when target is
I believe
Yes, definitely. And it seems the new decorators proposal address it with the new |
What do you propose should happen, today (or more accurately, in the near future), when the target is set to e.g. class C {
foo;
bar = 1;
}
|
I'd suggest the following behavior:
In such setup, people who prefer [[Define]] semantic should either set the compilation target to |
One more example, I've stumbled upon today. I've implemented various primitives on the pure-data export class Segment {
start : number = undefined
end : number = undefined
length : number = undefined
contains (point : number) : boolean {
return this.start <= point && point < this.end
}
} Now, I want to implement a "segment that represent a dimension of client rect of the DOM element" and be able to re-use the primitives from export class ElementDimension extends Segment {
el : Element = undefined
type : 'width' | 'height' = 'width'
get start () : number {
return this.type === 'width' ? this.el.getBoundingClientRect().left : this.el.getBoundingClientRect().top
}
set start (value : number) {
// readonly
} But TS compiler generates an unnecessary error, which is the scope of this proposal. |
@sandersn I believe the community feedback on this proposal is positive. What will be the next step for it? |
We'll take this up at a design meeting when we think that there's enough demand for it. However, right now it's two people's examples against a survey of many code bases across 3 large companies. If you can show quantitatively that demand is high, it would help. |
Its two people examples only in this thread. Plus 12 positive emojis. Plus many more examples in the previous thread, which itself has 12 negative emojis. #37894 (comment) +3 emojis This means in total, 2 + 12 + 12 +7 + 3 + 12 + 3 + 13 + 5 + 1 = 70 people are positive for this feature. This amount of developers are representative enough. Can you share what codebases were examined in the survey? Are they written in the OOP approach? Do the authors of those codebases have the JavaScript background, or they come from other languages? Because in our internal JavaScript code base, the property access augmentation pattern is used excessively. It is a valid "classic" JavaScript pattern, which assumes [[Set]] semantic for properties, its not clear, why TypeScript restricts it both for [[Define]] and [[Set]]. |
Keep in mind, we are talking about adding a single |
@sandersn ^^^ |
@sandersn If I'll contribute a PR for this proposal - will it be merged? |
We discussed this at the weekly suggestion backlog meeting. The bottom line is that Typescript needs to forbid mixed accessor/property overrides because they're broken by Ecmascript's use of [[Define]] for properties. And Typescript's primary goal is to type Javascript, not to emit slightly-improved Javascript. We delayed the switch to [[Define]] as long as we could — Typescript normally adopts proposals at stage 3 — but when class fields hit stage 4 and I found that the big code bases that we surveyed only mixed access/property overrides by mistake, we decided that the best option was to prevent future code from relying on [[Set]]. Some of the code bases that I'm aware of that are heavily OO:
My Google and Bloomberg contacts didn't share the projects that they surveyed, but based on Bloomberg's Typescript contributions, I assume they use OO heavily. |
@sandersn Ok, thanks for the update. |
If we're being pedantic, this change singlehandedly prevents everyone from ever using public properties in any of their classes ever again, because it prevents others from ever extending their classes with new behaviour. When you're designing a class, it's impossible to know what someone else might wanna extend it with 10 years later. This change throws us back to the C++ hell of having useless public getters and setters for every attribute we'll ever make, just in case someone else might need to override the behaviour in the unforeseeable future. Worst change ever made in the history of the TypeScript repository. Other languages are fighting to implement property overriding, while TypeScript decided to proactively prevent it. |
It's for sure a VERY annoying issue, which prevent me to upgrading to typescript 4+, which is VERY annoying in itself for compatibility issue with various others libs. |
As suggested by @sandersn, opening a sibling issue to track the proposal from the #37894
Topic
Currently, when a property in the base class is overridden with a getter/setter pair, compiler always generates an error:
Playground link
This error is motivated by the fact, that in modern JavaScript, class fields have DEFINE semantic and the above example won't work as intuitively expected if
useDefineForClassFields
compiler option is set totrue
.The property access augmentation pattern
Need to note, that historically nobody was ever using the DEFINE semantic for class properties. Pretty much all the JavaScript and TypeScript code in the world currently assumes the SET semantic for class fields.
SET semantic makes it possible to use the property access augmentation pattern. Its main use case is to trigger some arbitrary code on property read/write. This is very useful for variety of purposes and is done by simply overriding the property of the base class with getter/setter accessors pair.
For example, lets say we have some 3rd party library with a class with
parent
property.In the whole library, the
parent
is assumed to be a regular property and is accessed with dot notation.Now lets say we want to plug this 3rd party library in our codebase, which has reactive capabilities. We need to track the access to the
parent
property and trigger some extra code on property write:With this simple subclass, we can use the 3rd party library code unchanged and mirror the writes to the
parent
property to thereactiveParent
(which is plugged into the rendering process elsewhere).DEFINE / SET semantic
TypeScript and JavaScript historically were using SET semantic for class fields. Pretty much all the code in the world assumes it. However, TC39 committee decided that class fields should use DEFINE semantic and to provide the way to migrate to new behavior, TypeScript introduced the new compiler config,
useDefineForClassFields
, disabled by default, because its a breaking change. Obviously, the "classic", historical behavior will need to be supported for a very long time.With this option enabled, the "property access augmentation" pattern becomes invalid, as demonstrated by the example in the beginning (see however, the "Additional considerations" below). It is probably reasonable to generate a compiler error being discussed in this case.
With this option disabled, the pattern remains valid. In this case there are no reasons to limit the language expressiveness and restrict the user from using the pattern.
Proposal
Proposal is to generate the compiler error being discussed only for the "DEFINE" semantic (
useDefineForClassFields : true
) case.Additional considerations
There's a promise from TC39 committee to provide an opt-in escape route to the "classic" SET semantic for class fields, using a decorator. This means, that the "property access augmentation" pattern will probably remain valid in JavaScript, even for native class fields, a user will just need to opt-in for it, using the decorator.
Ideally, compiler will need to be smart, to determine which semantic the field is actually using, to avoid the needless restrictions. Perhaps the nature of this error is that it should be handled better by the linter, rather than compiler.
The text was updated successfully, but these errors were encountered: