This library exports ECMAScript decorators that adds support for parameter decorators
Only standard decorators are supported, not TypeScript experimental decorators;
it's tested with both Babel's @babel/plugin-proposal-decorators and TypeScript 5+.
Note that below, when it's said that a parameter decorator is conditionally applied, or that it will be applied to specific values, this is only about the function that such a parameter decorator can return. The parameter decorator itself will only be called once, and unconditionally, when the class is initialized; the returned function will be called (or maybe not) each time the annotated method is invoked.
The parameters decorator can annotate classes or class methods and allows applying parameter decorators to their parameters (or the parameters of the class constructor).
Note that because the parameter name, or whether the parameter is a rest parameter of a variadic function, cannot be determined at runtime, those informations must be specified explicitly.
The parameters decorator takes as many arguments as the function it annotates, where each argument represents the decorator(s) to apply to the corresponding function parameter.
Those can thus take different forms:
undefinedto skip the parameter and not apply any decorator to it- a single parameter decorator
- an array of parameter decorators, optionally prefixed with the parameter name and/or whether it's a rest parameter (both optional but in this order)
- an object with a
decoratorsproperty as an array of parameter decorators, and optional propertiesnameandrest
The following example, using the syntax from the ECMAScript proposal:
class Cls {
constructor(@A param1, param2, @B @C param3) { }
method(@D @E param1, @F @G param2, @H ...rest) { }
}can be written using the parameters decorator as (examplifying the various forms):
@parameters(A, undefined, { name: "param3", decorators: [B, C] })
class Cls {
constructor(param1, param2, param3) {}
@parameters(["param1", D, E], [F, G], ["rest", true, H])
method(param1, param2, ...rest) {}
}The parameter decorator is the equivalent of parameters but for a setter.
Because there can only be a single parameter, it takes an optional parameter name as the first argument, followed by a list of parameter decorators.
It can also take a single object as argument with a decorators property as an array of parameter decorators, and an optional name property.
class Cls {
@parameter("value", A, B)
set prop1(value) {}
@parameter(A)
set prop2(value) {}
}The rest parameter decorator allows applying a set of parameter decorators to each value of a rest parameter (each argument of the actual invocation) rather than the array of values.
class Cls {
@parameters([true, rest(A, B)])
method(...rest) {}
}
// The A and B decorators will each be called 3 times,
// for each one of the "one", "two", and "three" values.
new Cls().method("one", "two", "three");The defaultValue parameter decorator allows assigning a default value to an optional parameter before other decorators (parameter or class/method/setter) are applied.
Because decorators are applied first-to-last, this decorator should generally come first.
If other decorators (parameter or class/method/setter) don't need to see that default value, then you should prefer the native default parameter value syntax in the function declaration. In TypeScript, you'll want to use both to get the appropriate typing for the method (but note that the value declared in the function signature will actually be ignored).
class Cls {
// The decorator can see an `undefined` value
@parameters(A)
method1(param = -1) {}
// The decorator will never see an `undefined` value
@parameters([defaultValue(-1), A])
method(param) {}
}The optional parameter decorator allows only applying a set of parameter decorators to argument values that aren't undefined, i.e. when an optional parameter is not omitted.
class Cls {
@parameters(optional(A, B))
method(param) {}
}
// The A and B decorators won't be called
new Cls().method();
// The A and B decorators will be called with the 42 value
new Cls().method(42);Type inference should work in most cases, infering e.g. the method signature the decorator can be applied to from the parameter decorators (inference taking into account the parameter types of course, but also restrictions on the annotated method: its name, or whether it must static and/or private; as well as the containing class' shape, inferred from the decorators' context parameter). There are exceptions though, notably as soon as a parameter decorator has overloads (due to a design limitation of TypeScript in this case). To cope for those cases, the exported functions have overloads designed for explicit typing. You must then pass parameter types as type arguments, and can also pass the containing class' shape and method description as additional type arguments to check that the parameter decorators are used correctly.
class Cls {
@parameters<[number, boolean, string]>(A, B, C)
fn(a: number, b: boolean, c: string) {}
@parameters<[number, boolean], string>(A, B, [true, C])
variadic(a: number, b: boolean, ...rest: string[]) {}
@parameter<string>(C)
set prop(value: string) {}
// A "full" declaration could be:
@parameters<
[number], // ← non-variadic parameters
string, // ← rest parameter
Cls, // ← containing class
// ↓ method description
{ kind: "method"; name: "m"; private: false; static: false }
>(A, [true, C])
m(a: number, ...rest: string[]) {}
}