The Practical Client
Declarative Programming in TypeScript with Decorators
If you've used attributes in your C# or Visual Basic programs, then decorators in TypeScript are going to look very familiar to you. They're still in development, but here's a look both at how to use them and how to write them.
First, a little history: As I discussed in an earlier column, as part of creating Angular 2, the Google team moved away from its own typed version of JavaScript (called AtScript) and adopted TypeScript. That triggered some additions to TypeScript, including a proposal to add annotations (part of AtScript) to TypeScript.
In AtScript, annotations are an example of declarative programming in action. Rather than write out the procedural code to make something happen everywhere you need it, you write the code once and then declare (through an annotation) where you want that code to be run. You may not have to even write the code: It might be part of the framework the code runs inside of. It's a great way to handle crosscutting concerns (things that are needed in many places in the application but not in every place). If this sounds like the way that attributes are used in the Microsoft .NET Framework, then you've grasped the concept (think of the ASP.NET Authorize action filter, for example).
However, over at the JavaScript standards body, there's an equivalent proposal called decorators and it's under that name that this feature is being incorporated into TypeScript. How powerful are decorators? Well, using a decorator on a class allows you to modify or replace a class definition (or just observe the class). That seems pretty powerful to me.
Configuring Visual Studio
You can start using decorators in your TypeScript applications right now (at least, if you're using a version later than 1.5 -- I'm working with version 1.8). However, as soon as Visual Studio recognizes that you're using a decorator, you'll get a red wavy line under your code and a message that this is new technology and that it might change over time. You can make the message go away by setting the experimentalDecorators option to true.
Unfortunately, setting the experimentalDecorators option isn't as easy as you might think. You can go to your ts.config file and add the option there. This code would do the trick:
"compilerOptions":
{
"experimentalDecorators": false,
But, if you're working in a Visual Studio 2015 project, Visual Studio will ignore your ts.config file because it only looks at the settings in your project file. You might think you could set this option through your project's Properties Page … but you'd be wrong. At least as I write this, the experimentalDecorators option isn't available on your Project Properties page. If that's true for you also, then here's what you you'll need to do:
-
Right-click on your project in Solution Explorer and select Unload Project.
-
After the project is marked as 'unavailable', right-click on your project and select Edit
. Your project file will open in a Visual Studio tab.
-
Find the
that contains the element
-
Add this element to the group:
<TypeScriptExperimentalDecorators>true</TypeScriptExperimentalDecorators>
-
Close the editor tab, saving your changes.
-
6. Right-click on your project and select Reload Project.
You can also set the parameter by passing it in the command line to the TypeScript compiler:
tsc --experimentalDecorators
Using and Creating Decorators
To apply a decorator to a TypeScript component you use the @ sign. This example applies a decorator called audit to a class called Product:
@audit
class Product
{
constructor() { }
id: string;
price: number;
...more members...
Decorators can be applied to five types of TypeScript components: classes, methods, properties, accessors (getters and setters) and parameters.
You can apply multiple decorators to the same component either by listing the decorators one after another or applying them all on a single line. This set code applies the audit, error and manage decorators to the Product class:
@audit
@error @manage
class Product
To create a decorator you simply define a function with the name of your decorator and have it accept one or more parameters. The number of parameters your decorator function will need to accept will vary depending on what kind of component you're decorating:
- A class decorator must accept a single parameter: the class' constructor.
- Method and accessor decorators must accept three parameters: the class' prototype (unless the method is static, in which case you're passed the class' constructor), the name of the method, and a property descriptor.
- Property decorators must accept just two parameters: the class' prototype or constructor (depending on whether the property is/isn't static) and the property name.
- Parameter decorators must accept three parameters: the class prototype or constructor, the name of the member, and the index of the parameter.
TypeScript will refuse to use a decorator with a component unless the decorator accepts the appropriate parameters.
As an example, to define a class decorator all you need is a function that accepts a single parameter to hold the class' constructor. Because this is TypeScript, I'll want to data type that parameter and, given that the decorator is being passed a constructor, the best type is Function:
function audit(target: Function)
{
In a class decorator I can, among other options, replace the constructor. The following decorator first redefines the decorator function to allow me to return a value from function. Within my decorator, I define a new constructor, associate it with the object my decorator is applied to, and return it:
function initProd<returnFunc extends Function>(target: returnFunc): returnFunc
{
var ctor: Function;
ctor = function () {this.id = GetNextProductId();};
ctor.prototype = Object.create(target.prototype);
ctor.prototype.constructor = target;
return <returnFunc> ctor;
}
Customizing the Decorator
Rather than simply create a decorator, you can create a decorator factory that returns the decorator function to be used. Within the decorator factory, you just return the decorator function you want to use as an anonymous function. Unlike a plain decorator, a decorator function can accept parameters, which allows you to tailor the way the returned decorator behaves. This example chooses between two different functions based on a parameter called verboseLevel:
function audit(verboseLevel: string) {
if (verboseLevel == "verbose") {
return function (target: Object) { };
}
else {
return function (target: Object) { };
};
}
When calling a decorator factory, you have to provide parentheses. This code would work with my audit factory:
@audit("verbose")
class Product
As I noted, this technology is still in the proposal stage and isn't quite ready for prime time yet. However, by providing a way to handle crosscutting concerns, it's another step along the path to making TypeScript a fully featured development environment.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.