-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement the Stage 3 Decorators Proposal #50820
Conversation
At some point this flag should probably be aliased/renamed to the thought process I’m imagining is essentially, “ooh, I like ES decorators, I wonder if this will give me even cooler decorator features…” |
Maybe aliased, but probably not renamed so as not to break existing consumers. Also, parameter decorators are still experimental. |
Yeah, my point was more that at some point we’re going to have a flag called “experimental” that opts into legacy behavior, and worse, legacy behavior that’s incompatible with the standard behavior that’ll be supported by default. It’s a weird state of affairs and I can definitely foresee the future GH issues “I enabled experimentalDecorators and all my existing decorators stopped working correctly, I thought this would just unlock additional features” |
a3ec9ee
to
e87b89c
Compare
2d7e2b2
to
8c33a18
Compare
I have 2 questions:
|
That is the current behavior of the proposal, but an alternative is being discussed in tc39/proposal-decorators#465.
Class decorators can only return functions. You are welcome to open an issue at https://github.com/tc39/proposal-decorators if you believe this should be changed. |
831a5b2
to
673073c
Compare
The ES2022 downlevel wraps the class in a function in order to provide encapsulated access to a couple of private static bindings ( class MyClass {
@MyDecorator
static method() {}
}; ...is downlevelled to ES2022 like this... let MyClass = (() => {
let _staticExtraInitializers = [];
let _static_method_decorators;
return class MyClass {
static {
_static_method_decorators = [MyDecorator];
__esDecorate(this, null, _static_method_decorators, { kind: "method", name: "method", static: true, private: false }, null, _staticExtraInitializers);
__runInitializers(this, _staticExtraInitializers);
}
static method() { }
};
})(); Would it be possible/desirable to specialize the emit for ES2022+ to use class private bindings? That would eliminate the function-wrapping and slightly improve the debugging experience (smaller callstack + in the object inspector # privates have less noise than closures). class MyClass {
static #staticExtraInitializers = [];
static #method_decorators;
static {
this.#static_method_decorators = [MyDecorator];
__esDecorate(this, null, this.#static_method_decorators, { kind: "method", name: "method", static: true, private: false }, null, this.#staticExtraInitializers);
__runInitializers(this, this.#staticExtraInitializers);
}
static method() { }
}; |
I considered this early on. Except for instance "initializers", those private fields would be unused after class definition evaluation and would take up extra space on the class itself. In addition, those temporary values can't be garbage collected. I'd argue they also make debugging worse when interrogating a Watch window given the excess properties attached to the class. |
I just tried this with
I have tests that use a title decorator to override class names for test suite naming. // foo.tests.js
@testTitle("foo bar")
export class FooTests {
// test functions...
} Is this a bug or is there an option to allow this syntax for js?
Does this mean typescript files support this syntax? |
My understanding of the spec is that the above has to be written: export
@testTitle("foo bar")
class FooTests {
// test functions...
} So, we emit an error for the other form. But I believe for convenience, you can write it the "old way" in TypeScript code and it will emit it the other way around. |
In a We still think that placing decorators after |
I won't argue for using class privates in the implementation. There's just one bit of clarification.
In practice the storage costs and lifetimes are equivalent between class privates and closures. Due to a long-standing implementation detail in the way closures work in pretty much all engines, the lifetime of captured bindings matches the outer closure despite the fact that inner functions no longer have need for them. So with the current Decorators implementation, those temporary values (e.g. the So if we want to release that memory earlier, we need to release it manually by setting it to |
I noticed that with the nightly builds, declare function stringField(_: undefined, ctx: ClassFieldDecoratorContext<A, string>): void | ((this: A, value: string) => string)
declare function stringFieldVoid(_: undefined, ctx: ClassFieldDecoratorContext<A, string>): void
declare class A {
@stringField
foo: number
@stringFieldVoid // <- used to error, now doesn't anymore
bar: number
} The same code with a previous version of this PR: I did not test if this occurs with other |
Oh no - the removal of |
I have updated the comment. Declaring a full return type seems to fix this issue |
I'd hate to remove the type param. The only other option I can think of would be to reintroduce |
Using the return type instead of declare function objectField<T extends Record<string, any>>
(_: undefined, ctx: ClassFieldDecoratorContext<A>): void | ((value: T) => T)
declare class A {
@objectField
foo: {
a: string // <- Non-optional fields are not allowed here.
b?: {
c: number
}
}
@objectField
bar: {
a?: string
b?: {
c: number // <- Non-optional fields are allowed here.
}
}
} To me, this looks inconsistent: Nightly Playground Link This was not the case when using the type args on |
We could declare it as interface ClassFieldDecoratorContext<This = unknown, in out Value = unknown> Though I think originally, |
should we keep watching here for updates on this? |
I'd prefer people not continue discussing on the PR. The conversation is already very long, and it is hard to keep track of new issues. I opened up #52540 to track the last-minute Any other questions/comments should be filed as new issues. Thanks all! |
This implements support for the Stage 3 Decorators proposal targeting
ESNext
throughES5
(except where it depends on functionality not available in a specific target, such as WeakMaps for down-level private names).The following items are not currently supported:
--emitDecoratorMetadata
, as metadata is currently under discussion in https://github.com/tc39/proposal-decorator-metadata and has not yet reached Stage 3.declare
field.With that out of the way, the following items are what is supported, or is new or changed for Decorators support in the Stage 3 proposal:
--experimentalDecorators
flag will continue to opt-in to the legacy decorator support (which still continues to support--emitDecoratorMetadata
and parameter decorators).--experimentalDecorators
flag.ESNext
(or at least, until such time as the proposal reaches Stage 4).target
andcontext
:target
— A value representing the element being decorated:get
accessors, andset
accessors: This will be the function for that element.accessor x
): This will be an object withget
andset
properties.undefined
.context
— An object containing additional context information about the decorated element such as:kind
- The kind of element ("class"
,"method"
,"getter"
,"setter"
,"field"
,"accessor"
).name
- The name of the element (either astring
orsymbol
).private
- Whether the element has a private name.static
- Whether the element was declaredstatic
.access
- An object with either aget
property, aset
property, or both, that is used to read and write to the underlying value on an object.addInitializer
- A function that can be called to register a callback that is evaluated either when the class is defined or when an instance is created:get
andset
declarations) no longer receive the combined property descriptor. Instead, they receive the accessor function they decorate.get
/set
pairs can be found at https://github.com/tc39/proposal-grouped-and-auto-accessors.static
member, you can use:enumerable
,configurable
, orwritable
properties as they do not receive the property descriptor. You can partially achieve this viacontext.addInitializer
, but with the caveat that initializers added by non-static member decorators will run during every instance construction.This is not currently consistent in all cases and is only set when transforming native ES Decorators or class fields. While we generally have not strictly aligned with the ECMA-262 spec with respect to assigned names when downleveling classes and functions (sometimes your class will end up with an assigned name of
class_1
ordefault_1
), I opted to include this becausename
is one of the few keys available to a class decorator's context object, making it more important to support correctly.Type Checking
When a decorator is applied to a class or class member, we check that the decorator can be invoked with the appropriate target and decorator context, and that its return value is consistent with its target. To do this, we check the decorator against a synthetic call signature, not unlike the following:
The types we use for
T
,C
, andR
depend on the target of the decorator:T
— The type for the decoration target. This does not always correspond to the type of a member.{ get, set }
object corresponding to the generated get method and set method signatures.undefined
.C
— The type for the decorator context. A context type based on the kind of decoration type, intersected with an object type consisting of the target's name, placement, and visibility (see below).R
— The allowed type for the decorator's return value. Note that any decorator may returnvoid
/undefined
.T
.{ get?, set?, init? }
whoseget
andset
correspond to the generated get method and set method signatures. The optionalinit
member can be used toinject an initializer mutator function.
Method Decorators
A method decorator applied to
m(): void
would use the typesresulting in a call signature like
Here, we specify a target type (
T
) of(this: MyClass) => void
. We don't normally traffic around thethis
type for methods, but in this case it is important that we do. When a decorator replaces a method, it is fairly common to invoke the method you are replacing:You may also notice that we intersect a common context type, in this case
ClassMethodDecoratorContext
, with a type literal. This type literal contains information specific to the member, allowing you to write decorators that are restricted to members with a certain name, placement, or accessibility. For example, you may have a decorator that is intended to only be used on theSymbol.iterator
method, or one that is restricted to
static
fields, or one that prohibits usage on private members
We've chosen to perform an intersection here rather than add additional type parameters to each *DecoratorContext type for several reasons. The type literal allows for a convenient way to introduce a restriction in your decorator code without needing to fuss over type parameter order. Additionally, in the future we may opt to allow a decorator to replace the type of its decoration target. This means we may need to flow additional type information into the context to support the
access
property, which acts on the final type of the decorated element. The type literal allows us to be flexible with future changes.Getter and Setter Decorators
A getter decorator applied to
get x(): string
above would have the typesresulting in a call signature like
, while a setter decorator applied to
set x(value: string)
would have the typesresulting in a call signature like
Getter and setter decorators in the Stage 3 decorators proposal differ significantly from TypeScript's legacy decorators. Legacy decorators operated on a
PropertyDescriptor
, giving you access to both theget
andset
functions as properties of the descriptor. Stage 3 decorators, however, operate directly on theget
andset
methods themselves.Field Decorators
A field decorator applied to a field like
#x: string
above (i.e., one that does not have a leadingaccessor
keyword) would have the typesresulting in a call signature like
The
target
of a field decorator is alwaysundefined
, as there is nothing installed on the class or prototype during declaration evaluation. Non-static
fields are installed only when an instance is created, whilestatic
fields are installed only after all decorators have been evaluated. This means that you cannot replace a field in the same way that you can replace a method or accessor. Instead, you can return an initializer mutator function — a callback that can observe, and potentially replace, the field's initialized value prior to the field being defined on the object:This essentially behaves as if the following happened instead:
Auto-Accessor Decorators
Stage 3 decorators introduced a new class element known as an "Auto-Accessor Field". This is a field that is transposed into pair of
get
/set
methods of the same name, backed by a private field. This is not only a convenient way to represent a simple accessor pair, but also helps to avoid issus that occur if a decorator author were to attempt to replace an instance field with an accessor on the prototype, since an ECMAScript instance field would shadow the accessor when it is installed on the instance.An auto-accessor decorator applied to a field like
accessor y: string
above would have the typesresulting in a call signature like
Note that
T
in the example above is essentially the same as, while
R
is essentially the same asThe return value (
R
) is designed to permit replacement of theget
andset
methods, as well as injecting an initializer mutator function like you can with a field.Class Decorators
A class decorator applied to
class MyClass
would use the typesresulting in a call signature like
Fixes #48885