Study Note with Decorator In TypeScript (3) - Class Decorator

Definition of Class Decorator

According to the definition from TypeScript documentation

A Class Decorator is declared just before a class declaration. The class decorator is applied to the constructor of the class and can be used to observe, modify, or replace a class definition. A class decorator cannot be used in a declaration file, or in any other ambient context (such as on a declare class).

Very similar to property decorator and method decorator, a class decorator is actually a special function accepting a particular parameter. In a class decorator, the particular parameter is the constructor of the class.

Simple Example

Scenario

There is a human resource management system which has multiple different modules such as leave management (LM), payroll management (PM) or candidate management (CM) with various module code as identifiers. A particular users may have different authorizations in different modules. For example, a normal payroll staff can only have create, read and update rights in payroll management while his/her boss may have delete and approve rights besides the mentioned access control types.

In this example, our purpose is to utilize the class decorator to inject the access control object to the payroll management module class.

Access Control Matrix in AuthService

First, we assume that our server has already returned an access control matrix to the client, and the json data is stored in AuthService.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AuthService {
// Assume the server returns the access matrix of the current user as below
private accessMatrix = {
PM: ['create', 'read', 'update'],
LM: ['read'],
CM: [],
};

public getAccessMatrix(moduleCode: string) {
return this.accessMatrix[moduleCode];
}
}

// For simplicity, an instance of AuthService is created as a global variable.
// Normally in Angular, it will be instanitiated in a provider and injected into the component
const authService = new AuthService();

This snippet indicates that the current user only has create, read and update access rights to payroll management module and read access right to leave management module.

And then an instance of AuthService is created for subsequent use.

Sample Classes For X Management Modules

Then we create new classes for payroll management module, candidate management module and leave management module. All of them have identical methods which can print what access types the current user has.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PayrollManagementModule {
public accessTypes;

public queryWhatICanDo() {
console.log(`You can do ${this.accessTypes.length ? this.accessTypes.join(', ') : 'nothing'} in this module`);
}
}

class LeaveManagementModule {
public accessTypes;

public queryWhatICanDo() {
console.log(`You can do ${this.accessTypes.length ? this.accessTypes.join(', ') : 'nothing'} in this module`);
}
}

class CandidateManagementModule {
public accessTypes;

public queryWhatICanDo() {
console.log(`You can do ${this.accessTypes.length ? this.accessTypes.join(', ') : 'nothing'} in this module`);
}
}

Create A Class Decorator and Use Them to Module Classes

Now we have three module classes. Then, we would like to use a class decorator to automatically grab the particular access type using the module code from AuthService and put into the module as a property.

Now let’s implement the class decorator.

1
2
3
4
5
6
7
function AccessControl(moduleCode: string) {
return function<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
accessTypes = authService.getAccessMatrix(moduleCode);
};
};
}

As indicated in the code snippet, our class decorator is named as AccessControl and it accepts one custom parameter moduleCode. It is actually a wrapper of the actual class decorator. The reason of doing so is because

  • a class decorator only accept the constructor of the decorated class as a parameter, and
  • we want to put the module code as a custom parameter to determine its access type

Therefore, in the wrapper class, an actual class decorator is returned. In this class decorator, it simply assign the access types information from AuthService instance we created just now, to the accessTypes property of the decorated class.

And then, we update the classes as below

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
@AccessControl('PM')
class PayrollManagementModule {
public accessTypes;

public queryWhatICanDo() {
console.log(`You can do ${this.accessTypes.length ? this.accessTypes.join(', ') : 'nothing'} in this module`);
}
}

@AccessControl('LM')
class LeaveManagementModule {
public accessTypes;

public queryWhatICanDo() {
console.log(`You can do ${this.accessTypes.length ? this.accessTypes.join(', ') : 'nothing'} in this module`);
}
}

@AccessControl('CM')
class CandidateManagementModule {
public accessTypes;

public queryWhatICanDo() {
console.log(`You can do ${this.accessTypes.length ? this.accessTypes.join(', ') : 'nothing'} in this module`);
}
}

Now, every module is decorated with AccessControl and their module code. Now, their accessTypes property can be automatically value assigned from the service.

Instanitiate Module Classes and Check the Results

1
2
3
4
5
6
7
const payrollManagement = new PayrollManagementModule();
const leaveManagement = new LeaveManagementModule();
const candidateManagement = new CandidateManagementModule();

payrollManagement.queryWhatICanDo();
leaveManagement.queryWhatICanDo();
candidateManagement.queryWhatICanDo();

Compile the TypeScript code and then run the JavaScript, you would get the following text printed in the console:

1
2
3
You can do create, read, update in this module
You can do read in this module
You can do nothing in this module

The results exactly matches our access control configuration in AuthService!

Conclusion

This concludes explanation of class decorator and its simple application. You may find the source code for the sample here: Demo in GitHub Repo