This post takes an in-depth look at parameter decorators. It examines their signature and provides a couple of useful examples. Reading the previous posts in the series is encouraged but not necessary.
The Series so Far
- Decorator Introduction
- JavaScript Foundation
- Reflection
- Parameter Decorators
- Property Decorators
- Method Decorators
- Class Decorators
Eventual Topics:
- Where Decorators Work
- Decorating Instance Elements vs. Static Elements
- Examples
- Pairing Parameter Decorators with Method Decorators
- Pairing Property Decorators with Class Decorators
Code
You can view the code related to this post under the post-04-parameter-decorators
tag.
Overview
Parameter decorators are the most restricted decorators. The official docs state
[a] parameter decorator can only be used to observe that a parameter has been declared on a method.
Parameter decorators ignore any return, underscoring their inability to affect the decorated parameters. As we saw previously, parameter decorators can be used in tandem with other decorators to define extra information about the parameter. By themselves, their effectiveness is limited. Logging parameter data seems to be the best use for a parameter decorator by itself.
(If you’ve got a different or novel use for parameter decorators, I’d love to hear about it. Seriously. I’m really curious to see how other devs are using these. My email’s in the footer.)
Class Method vs Global Function
An interesting side-effect of decorators is that they (apparently) must be defined on class elements. You can’t decorate globals unattached to a class.
class-only.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | function DecoratedParameter( |
$ ts-node class-only.ts |
Even though we’ve attempted to decorate the global function foo
, it doesn’t work. Notice how the decorated logging is only called once, not twice, and only with foo1
. I suspect this is related to how all of these things are defined, and I plan to investigate this more in another post.
Signature
signature.ts | |
1 2 3 4 5 | type ParameterDecoratorType = ( |
This example is used to explain the signature.
signature-example.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 | function DecoratedParameter( |
target: any
target
is the object (not method) that owns the method whose parameter has been decorated. target
in the example is TargetDemo
, not foo
.
propertyKey: string | symbol
propertyKey
is the method name (not object name) whose signature has been decorated. It could also be a Symbol
, depending on how the method is defined on the object. propertyKey
in the example is foo
, not TargetDemo
.
parameterIndex: number
parameterIndex
is the index of the decorated parameter in the signature of the calling method. parameterIndex
in the example is 1
.
Usage
I spent last week trying to figure out an interesting or useful parameter decorator that functions in a vacuum, i.e. one not used with other decorators (well, not the whole week, just when I wanted to work on a really difficult problem that doesn’t seem to have a good solution). I still have nothing. Parameter decorators are triggered when the parameter is declared, but they don’t affect anything. We can’t observe the parameter’s value, because that’s attached long after the parameter is decorated. We can’t change the state, because that’s also not created until long after the parameter is decorated. Long story short, we can define metadata and that’s about it.
If you haven’t read the reflection post, give it a quick skim. We’ll either have to build our own metadata interface in vanilla TypeScript or use the reflect-metadata
package. One requires a bunch of extra work totally unrelated to the code we’re writing and the other is a simple import.
Once again (I’m getting tired of reiterating this), parameter decorators are observers. We can define metadata, but we’re not really able to consume any. Parameter decorators are executed before anything else, so I suppose you could consume other parameter metadata but that’s just silly (I’d wager that execution order isn’t well-defined across platforms, modules, and standards).
required
The official docs give a very useful example. One of the features TypeScript adds is required arguments, e.g. if I define function foo(bar: string)
, I can’t compile foo()
. However, the underlying JavaScript doesn’t respect those restrictions. Anything downstream that uses the JavaScript instead of the TypeScript could easily sidestep those restriction (accidentally or not), and there are plenty of ways around them in TypeScript itself.
Using decorators, we can at least note that parameter is required or not. Whether or not something is done with that metadata is outside the scope of parameter decorators, so I’m skipping that here. This is one way to tag them. It’s loosely based on the official docs but approaches things differently enough that I’m comfortable calling this my own. Honestly there are only so many way to create an array, add values, and pass it on.
required/constants.ts | |
1 | export const REQUIRED_KEY = Symbol("requiredParameter"); |
By export
ing the Symbol
we can use it anywhere we import
it (and ensure it’s the same everywhere).
required/Required.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import "reflect-metadata"; |
You don’t actually have to sort
the array, but the order might not be what you expect (it was reversed the one time I ran it). If you’re consuming it via a for...of
loop, you really don’t have to sort
it.
required/main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import "reflect-metadata"; |
$ ts-node required/main.ts |
Arbitrary Metadata
I’ve already written an example adding some validation metadata. The official docs cover pulling existing metadata. You can basically add anything you’d like.
The example below illustrates two different approaches to add specific parameter metadata. You can either create a decorator that takes everything (ParameterMetadata
) or chain individual decorators (Name
, Description
) to attach only the desired information (of course you could tweak ParameterMetadata
’s signature to request an object and pull name
and description
out of that instead).
arbitrary/constants.ts | |
1 2 3 | export const PARAMETER_NAME_KEY = Symbol("parameterName"); |
First we define the metadata keys and export
them for anything to import
.
arbitrary/interfaces.ts | |
1 2 3 4 5 6 | export interface IParameterMetadata { |
This defines IParameterMetadata
and aliases an array of IParameterMetadata
as SignatureMetadataType
to streamline manipulation. It’s usually better to have types to rely on.
arbitrary/Name.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | import "reflect-metadata"; |
The Name
decorator updates the list of parameter names and also updates the list of signature metadata, since I decided to make things complicated.
arbitrary/Description.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | import "reflect-metadata"; |
The Description
decorator is almost identical to Name
. It, rather unsurprisingly, updates descriptions instead of names.
arbitrary/ParameterMetadata.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | import "reflect-metadata"; |
The ParameterMetadata
decorator updates both names and descriptions as well as signature metadata. As I mentioned earlier, it would be fairly straightforward to update its signature to request an IParameterMetadata
object (instead of [string, string]
), but I didn’t think of that until I started annotating the example so I didn’t do that.
arbitrary/main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import "reflect-metadata"; |
Putting everything together, we can use any of the decorators we’d like. We could chain any combination we’d like, but it’s important to remember how decorator chaining works; essentially the outermost (first, top, whatever) decorator will overwrite anything set by inner decorators.
$ ts-node arbitrary/main.ts |
Recap
Parameter decorators are great at adding extra information about parameters at runtime. They can’t do a whole lot more. Parameter decorators are often used in combination with other decorators to perform new actions at runtime.
Legal
The TS logo is a modified @typescript
avatar; I turned the PNG into a vector. I couldn’t find the original and I didn’t see any licensing on the other art, so it’s most likely covered by the TypeScript project’s Apache 2.0 license. Any code from the TS project is similarly licensed.
If there’s a problem with anything, my email’s in the footer. Stay awesome.