Create class decorators in TypeScript
In the previous part
of this article and video series you learned about TypeScript inheritance. In
this article you will learn about a feature of TypeScript that allows you to
annotate and modify classes and class members. This feature is called
Decorators. Decorators is an experimental feature and you need to enable them in
your TypeScript configuration file. You will find TypeScript decorators
analogous to C# attributes in that they form the metadata of a class and class
members.
Enabling decorators
As mentioned earlier, decorators is an experimental feature and you need to
enable them in TypeScript configuration. To do so, open tsconfig.json file from
the wwwroot/TypeScript folder and modify it as shown below:
{
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "es5",
"experimentalDecorators": true,
"outDir": "./Output"
},
"compileOnSave": true,
"exclude": [
"node_modules",
"wwwroot"
]
}
If you are following this article series, the above settings should look
familiar to you. Notice the setting marked in bold letters. The
experimentalDecorators key is set to true indicating that decorators are enabled
for your project.
Types of decorators
Before developing our own decorators let me quickly show you how they look
like so that you get an idea. Take a look at the following class declaration:
@Theme
class Employee{
@Required
employeeID : number;
@Required
fullName : string;
@Track
showDetails() : void {
}
}
Notice the code marked in bold letters. They are decorators. You can see that
Employee class is annotated with @Theme decorator. The employeeID and fullName
properties are annotated with @Required decorator. And showDetails() method is
annotated with @Track decorator. At code level decorators are functions. So, in
the above code somewhere Theme, Required, and Track functions exist that do the
intended processing.
In the above code decorators are added to class, properties, and methods. You
can also add them to accessors and function parameters. Depending on which
entity you want to decorate, you have various flavors of decorators such as
class decorators, property decorators, and method decorators.
Class decorators
Now that you have some idea about what decorators are, let's create our first
class decorator. In the following example you will create a class decorator
called Theme. The Theme decorator simply adds a set of properties to the class
definition that hold certain CSS classes.
Firstly, add a new TypeScript file to the TypeScript folder named
ClassDecorators.ts.
Then add the following Employee class definition to it.
class Employee {
employeeID: number;
fullName: string;
employeeID_class: string;
fullName_class: string;
constructor(id,name) {
this.employeeID = id;
this.fullName = name;
this.employeeID_class = "class1";
this.fullName_class = "class2";
}
showDetails(): void {
document.getElementById("employeeID").
className = this.employeeID_class;
document.getElementById("employeeID").
innerHTML = this.employeeID.toString();
document.getElementById("fullName").
className = this.fullName_class;
document.getElementById("fullName").
innerHTML = this.fullName;
}
}
As you can see the Employee class contains four properties - employeeID,
fullName, employeeID_class, and fullName_class. The first two properties contain
the respective values whereas the last two properties hold names of CSS classes
to be used while displaying these values. These four properties are assigned
values in the constructor.
The showDetails() method picks certain HTML elements from the HTML page
(shown later) and sets their className and innerHTML properties.
The HTML page housing these elements (ClassDecorator.html) looks like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link href="/TypeScript/StyleSheet.css"
rel="stylesheet" />
</head>
<body onload="DecoratorDemo()">
<div id="employeeID"></div>
<div id="fullName"></div>
<script src="/TypeScript/Output/ClassDecorator.js">
</script>
</body>
</html>
I am not discussing the content of StyleSheet.css here because it just
contains class1 and class2 CSS classes.
The DecoratorDemo() function that is called upon body load is shown below:
function DecoratorDemo() {
let emp = new Employee(1,'Nancy Davolio');
emp.showDetails();
}
The above code simply creates an object of Employee class by passing
employeeID and fullName values. It then calls the showDetails() function. A
sample run of the page is shown below:
So far so good.
Modifying class properties
Now let's create a decorator that replaces the employeeID_class and
fullName_class properties with new values.
Add the Theme() function in the ClassDecorator.ts file as shown below:
function Theme<T extends {
new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
employeeID_class = "class3";
fullName_class = "class4";
}
}
The Theme() function receives the constructor of the class being decorated
(Employee in this case) as its parameter.
Inside, it extends the supplied constructor and assigns some different values
to the employeeID_class and fullName_class properties.
Next, apply Theme decorator to the Employee class as shown below:
@Theme
class Employee {
...
...
}
Notice that decorators take the form of @ followed by the decorator function
name.
The following figure shows a sample run of the page after @Theme decorator
has been applied.
As you can see, the @Theme decorator has overridden the CSS class names and
hence <div> elements are shown in different color.
Decorator factories
In the above example the @Theme decorator doesn't accept any parameters. What
if we want to pass the new CSS class names as decorator parameters? You can
accomplish this by creating a decorator factory.
Let's see how.
function ThemeEx(settings: any) {
let func = function <T extends
{ new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
employeeID_class = settings.employeeID_class;
fullName_class = settings.fullName_class;
}
}
return func;
}
We created another decorator called ThemeEx. This time the decorator function
accepts a JavaScript object containing the required CSS class names. Inside, we
create an anonymous function that extends the class constructor and overrides
the two properties as before. This code is identical to the Theme() function
written earlier except that instead of hard-coding the CSS class names, it picks
them from the outer function's settings parameter. Thus ThemeEx() function acts
like a factory that creates the actual decorator function.
To apply @ThemeEx decorator to the Employee class you can write the
following:
@ThemeEx({
employeeID_class: "class3",
fullName_class: "class6"
})
class Employee {
...
...
}
As you can see, we use @ThemeEx decorator and pass a JavaScript object with
two properties employeeID_class and fullName_class.
The output will be identical to the previous run of the page.
Adding new properties
In the example just discussed we overrode the Employee class properties. It
is also possible to add new properties to the class using decorators. Consider
the following modified Theme decorator:
function Theme<T extends
{ new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
employeeID_class = "class3";
fullName_class = "class6";
border_class = "class7";
}
}
Here, we added a new property named border_class and assigned some CSS class
name to it.
Since border_class is not a part of the design time Employee class you can't
access it like other properties of Employee. For example, the following code
gives error:
However, you can access the border_class by casting it as any type. Consider
this code:
showDetails(): void {
document.getElementById("employeeID").
className = this.employeeID_class;
document.getElementById("employeeID").
innerHTML = this.employeeID.toString();
document.getElementById("fullName").
className = this.fullName_class;
document.getElementById("fullName").
innerHTML = this.fullName;
if ((<any>this).hasOwnProperty
("border_class")) {
document.documentElement.className =
(<any>this).border_class;
}
}
Notice the code shown in bold letters. It uses hasOwnProperty()
JavaScript method to determine whether border_class property exists in the
class. If you don't add the @Theme decorator border_class won't be there and
hence this check is required. Then you access the border_class property and
assign the className of the documentElement.
A sample run of the above code is shown below:
You can also watch the companion video
here. In the next
part of this series you will learn about property and method decorators.
That's it for now! Keep coding!!