Implement the Stage 3 Decorators Proposal by rbuckton · Pull Request #50820 · microsoft/TypeScript (original) (raw)
This implements support for the Stage 3 Decorators proposal targeting ESNext
through ES5
(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.- Decorators on a
declare
field. - Parameter decorators will not be supported until a follow-on proposal has been adopted and has advanced to Stage 3.
- See Parameter decorators tc39/proposal-decorators#47 for additional information.
- Decorators may not change the type of the member or class they decorate. If we decide to allow this capability, we will do so in a later PR. This is under discussion in the following two issues:
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:
- The
--experimentalDecorators
flag will continue to opt-in to the legacy decorator support (which still continues to support--emitDecoratorMetadata
and parameter decorators). - ES Decorators are now supported without the
--experimentalDecorators
flag. - 🆕 ES Decorators will be transformed when the target is less than
ESNext
(or at least, until such time as the proposal reaches Stage 4). - 🆕 ES Decorators now accept exactly two arguments:
target
andcontext
:target
— A value representing the element being decorated:
* Classes, Methods,get
accessors, andset
accessors: This will be the function for that element.
* Auto-Accessor fields (i.e.,accessor x
): This will be an object withget
andset
properties.
* Fields: This will always beundefined
.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:
* For static member decorators, initializers run after class decorators have been applied but before static fields are initialized.
* For Class Decorators, initializers run after all static initializers.
* For non-static member decorators, initializers run in the constructor before all field initializers are evaluated.
- 🆕 ES Decorators can decorate private fields.
- 🆕 ES Decorators can decorate class expressions.
- ‼️ ES Accessor Decorators (i.e., for
get
andset
declarations) no longer receive the combined property descriptor. Instead, they receive the accessor function they decorate.- A stage 1 proposal that expands upon auto-accessors to allow you to decorate
get
/set
pairs can be found at https://github.com/tc39/proposal-grouped-and-auto-accessors.
- A stage 1 proposal that expands upon auto-accessors to allow you to decorate
- ‼️ ES Member Decorators (i.e., for accessors, fields, and methods) no longer have immediate access to the constructor/prototype the member is defined on.
- If you need access to the class constructor from a
static
member, you can use:
context.addInitializer(function() { this /constructor reference/ }); - If you need access to the instance (not the prototype) from a non-static member, you can use:
context.addInitializer(function() { this /instance reference/ }); - Non-static members currently have no way to access the constructor or prototype during class definition.
- This behavior is under discussion at Suggestion: Allow member decorators to add both static and instance *extra* initializers. tc39/proposal-decorators#465.
- If you need access to the class constructor from a
- ‼️ ES Member Decorators can no longer set the
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. - When the name of the class is inferred from an assignment, we will now explicitly set the name of the class in some cases.
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 ofclass_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:
type SyntheticDecorator<T, C, R> = (target: T, context: C) => R | void;
The types we use for T
, C
, and R
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.- For a class decorator, this will be the class constructor type.
- For a method decorator, this will be the function type of the method.
- For a getter decorator, this will be the function type of the get method, not the type of the resulting property.
- For a setter decorator, this will be the function type of the set method, not the type of the resulting property.
- For an auto-accessor field decorator, this will be a
{ get, set }
object corresponding to the generated get method and set method signatures. - For a normal field decorator, this will always be
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
.- For a class, method, getter, or setter decorator, this will be
T
. - For an auto-accessor field decorator, this will be a
{ get?, set?, init? }
whoseget
andset
correspond to the generated get method and set method signatures. The optionalinit
member can be used to
inject an initializer mutator function. - For a normal field decorator, this can be an initializer mutator function.
- For a class, method, getter, or setter decorator, this will be
Method Decorators
class MyClass { m(): void { ... } }
A method decorator applied to m(): void
would use the types
type T = (this: MyClass) => void; type C = & ClassMethodDecoratorContext<MyClass, (this: MyClass) => void> & { name: "m", private: false, static: false }; type R = (this: MyClass) => void;
resulting in a call signature like
type ExpectedSignature = ( target: (this: MyClass) => void, context: & ClassMethodDecoratorContext<MyClass, (this: MyClass) => void> & { name: "m", private: false, static: false }, ) => ((this: MyClass) => void) | void;
Here, we specify a target type (T
) of (this: MyClass) => void
. We don't normally traffic around the this
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:
function log<T, A extends any[], R>(
target: (this: T, ...args: A) => R,
context: ClassMethodDecoratorContext<T, (this: T, ...args: A) => R>
) {
return function (this: T, ...args: A): R {
console.log(${context.name.toString()}: enter
);
try {
// need the appropriate this
return target.call(this, ...args);
}
finally {
console.log(${context.name.toString()}: exit
);
}
};
}
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 the Symbol.iterator
method
function iteratorWrap<T, V>( target: (this: T) => Iterable, context: ClassMethodDecoratorContext<T, (this: T) => Iterable> & { name: Symbol.iterator } ) { ... }
, or one that is restricted to static
fields
function lazyStatic<T, V>( target: undefined, context: ClassFieldDecoratorContext<T, V> & { static: true } ) { ... }
, or one that prohibits usage on private members
function publicOnly( target: unknown, context: ClassMemberDecoratorContext & { private: false } ) { ... }
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
class MyClass { get x(): string { ... } set x(value: string) { ... } }
A getter decorator applied to get x(): string
above would have the types
type T = (this: MyClass) => string; type C = ClassGetterDecoratorContext<MyClass, string> & { name: "x", private: false, static: false }; type R = (this: MyClass) => string;
resulting in a call signature like
type ExpectedSignature = ( target: (this: MyClass) => string, context: ClassGetterDecoratorContext<MyClass, string> & { name: "x", private: false, static: false }, ) => ((this: MyClass) => string) | void;
, while a setter decorator applied to set x(value: string)
would have the types
type T = (this: MyClass, value: string) => void; type C = ClassSetterDecoratorContext<MyClass, string> { name: "x", private: false, static: false }; type R = (this: MyClass, value: string) => void;
resulting in a call signature like
type ExpectedSignature = ( target: (this: MyClass, value: string) => void, context: ClassSetterDecoratorContext<MyClass, string> & { name: "x", private: false, static: false }, ) => ((this: MyClass, value: string) => void) | void;
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 the get
and set
functions as properties of the descriptor. Stage 3 decorators, however, operate directly on the get
and set
methods themselves.
Field Decorators
class MyClass { #x: string = ...; }
A field decorator applied to a field like #x: string
above (i.e., one that does not have a leading accessor
keyword) would have the types
type T = undefined; type C = ClassFieldDecoratorContext<MyClass, string> & { name: "#x", private: true, static: false }; type R = (this: MyClass, value: string) => string;
resulting in a call signature like
type ExpectedSignature = ( target: undefined, context: ClassFieldDecoratorContext<MyClass, string> & { name: "#x", private: true, static: false }, ) => ((this: MyClass, value: string) => string) | void;
The target
of a field decorator is always undefined
, as there is nothing installed on the class or prototype during declaration evaluation. Non-static
fields are installed only when an instance is created, while static
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:
function addOne( target: undefined, context: ClassFieldDecoratorContext<T, number> ) { return function (this: T, value: number) { return value + 1; }; }
class C { @addOne @addOne x = 1; } new C().x; // 3
This essentially behaves as if the following happened instead:
let f1, f2; class C { static { f1 = addOne(undefined, { ... }); f2 = addOne(undefined, { ... }) } x = f1.call(this, f2.call(this, 1)); }
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.
class MyClass { accessor y: number; }
An auto-accessor decorator applied to a field like accessor y: string
above would have the types
type T = ClassAccessorDecoratorTarget<MyClass, string>; type C = ClassAccessorDecoratorContext<MyClass, string> & { name: "y", private: false, static: false }; type R = ClassAccessorDecoratorResult<MyClass, string>;
resulting in a call signature like
type ExpectedSignature = ( target: undefined, context: ClassFieldDecoratorContext<MyClass, string> & { name: "#x", private: true, static: false }, ) => ((this: MyClass, value: string) => string) | void;
Note that T
in the example above is essentially the same as
type T = { get: (this: MyClass) => string, set: (this: MyClass, value: string) => void };
, while R
is essentially the same as
type R = { get?: (this: MyClass) => string, set?: (this: MyClass, value: string) => void, init?: (this: MyClass, value: string) => string };
The return value (R
) is designed to permit replacement of the get
and set
methods, as well as injecting an initializer mutator function like you can with a field.
Class Decorators
class MyClass { m(): void { ... } get x(): string { ... } set x(value: string) { ... } #x: string; accessor y: number; }
A class decorator applied to class MyClass
would use the types
type T = typeof MyClass; type C = ClassDecoratorContext & { name: "MyClass" }; type R = typeof MyClass;
resulting in a call signature like
type ExpectedSignature = ( target: typeof MyClass, context: ClassDecoratorContext & { name: "MyClass" } ) => typeof MyClass | void;
Fixes #48885