TypeScript Decorators: Parameter Decorators

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.

Code

Overview

[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.

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.

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.

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.

Arbitrary Metadata

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).

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.

publicdescriptionOnly(@Description("decorated with Description")propertyWithDescriptionOnly: any,){// do nothing}

publicusingParameterMetadata(@ParameterMetadata("decoratedWithParameterMetadata","decorated with ParameterMetadata",)decoratedWithParameterMetadata: any,){// do nothing}

publicchainingDecorators(@Name("decoratedViaChain")@Description("decorated with Name and Description")decoratedViaChain: any,){// do nothing}}

// These are not defined on the class// namesconsole.log("ArbitraryMetadata names:",Reflect.getMetadata(PARAMETER_NAME_KEY,ArbitraryMetadata,),);// descriptionsconsole.log("ArbitraryMetadata descriptions:",Reflect.getMetadata(PARAMETER_DESCRIPTION_KEY,ArbitraryMetadata,),);// signature metadataconsole.log("metadata from ArbitraryMetadata signatures:",Reflect.getMetadata(PARAMETER_METADATA_KEY,ArbitraryMetadata,),);// They're defined on an instanceconstdemoArbitraryMetadata=newArbitraryMetadata();

// This could be created via decorators// Since it requires more than parameter decorators, it's hardcodedconstMETHODS=["nameOnly","descriptionOnly","usingParameterMetadata","chainingDecorators",];

// Loop over each methodfor(constmethodofMETHODS){// Line break to make things easier to readconsole.log("---");// Log the parameter namesconsole.log(`${method} names:`,Reflect.getMetadata(PARAMETER_NAME_KEY,demoArbitraryMetadata,method,),);// Log the parameter descriptionsconsole.log(`${method} descriptions:`,Reflect.getMetadata(PARAMETER_DESCRIPTION_KEY,demoArbitraryMetadata,method,),);// Log the full signature metadataconsole.log(`${method} signature metadata:`,Reflect.getMetadata(PARAMETER_METADATA_KEY,demoArbitraryMetadata,method,),);}

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.

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.