Decorator trong TypeScript

Decorator trong TypeScript hướng dẫn cách tạo và sử dụng các loại decorator như class,factory. property và method decorator…


Decorator trong TypeScript


Giới thiệu decorator trong typescript

Decorator – trang trí, đính kèm, bổ sung – là cách thức gắn kèm 1 hàm với class, method, property, accessor. Mục đích của việc gắn vào là để chạy hàm trong runtime mỗi khi các khai báo được sử dụng. Hàm decorator có nhiệm vụ thay đổi, bổ sung cho đối tượng được decorate.

Cài đặt decorator

Để dùng được các decorator, bạn khai báo giá trị true cho thuộc tính experimentalDecorators  trong file tsconfig.json

{
    "compilerOptions": {
        ..., 
        "experimentalDecorators": true
    },
}

Khai báo decorator

Khai báo decorator đơn giản, bằng cách sử dụng cú pháp @tênhàm . Trong đó tenhàm là tên một hàm sẽ được gọi khi runtime.  Ví dụ

@f  @g method() { } 

hoặc

@f
@g
method() { } 

Các loại decorator

Có nhiều loại decorator như class, factory, property, accessor, parameter…

Class decorator

Là loại decorator được chỉ định ngay trước khai báo class. Class decorator gắn vào constructor của class để thay thế, mở rộng class, bổ sung thuộc tính cho class. 

Class decorator được gọi lúc class được khai báo chứ không phải lúc 1 instance của class được tạo.

Sử dụng class decorator

Ví dụ sau cho thấy class decorator được chạy 1 lần lúc gặp khai báo class, không chạy khi tạo các đối tượng.

function ThuCungEx(constructor: Function) {
  console.log("Đây là hàm ThuCung Ex");
}
@ThuCungEx
class ThuCung {
    constructor(private ten:string, private tuoi:number){}
}

let tc1 = new ThuCung('Nô nô', 9);
let tc2 = new ThuCung('Mập', 2);
console.log("tc1=", tc1)
console.log("tc2=", tc2)

Thêm thuộc tính vào class dùng class decorator

Để thêm thuộc tính phai, và ngaytao vào class HocVien, code trong decorator BaseHV và gắn vào class HocVien như sau:

function BaseHV( constructor: Function) {
    constructor.prototype.phai = true;
    constructor.prototype.ngaytao = new Date().toLocaleString('vi');
}
@BaseHV
class HocVien { constructor(public ht: string) {}  }
let hv1= new HocVien('Tèo'); console.log(hv1);

Thêm thuộc tính dùng class decorator

Dùng class decorator để return class mới mở rộng class hiện tại với các thuộc tính bổ sung

function themTT<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    mauxe:string = 'Xanh';
  };
}

@themTT
class XeMay { 
  constructor( private tx:string, private gia:number){ }
}

const x1 = new XeMay('Vision 125', 39.5); 
console.log(x1, x1["mauxe"]);

Ví dụ dùng class decorator đổi nội dung class

function ChangeHS(constructor: Function):any {
    return class {       
        private hoten:string;  
        public phai:boolean;
        constructor(h:string){
            this.hoten = h;    
            this.phai = true;
        }
    } 
}
@ChangeHS
class HocSinh {  
    public name: string;    
    constructor( h:string ) { this.name=h;}
}
let u1= new HocSinh("Tèo");  console.log(u1);

Sử dụng kết hợp nhiều class decorator

function BaseUser1(constructor: Function) {
    console.log(`Đây là hàm BaseUser 1`);
}
function BaseUser2(constructor: Function) {
    console.log(`Đây là hàm BaseUser 2`);
}

@BaseUser1 @BaseUser2
class User { constructor(public name: string) {} }
let u1 = new User('Lượm'); 
console.log(u1);

Decorator factory

Đây là một hàm trả về chính hàm decorator. Cụ thể, decorator factory bao bọc quanh 1 hàm decorator để truyền tham số cho nó. Nhờ đó mà việc sử dụng các decorator sẽ uyển chuyển hơn.

Ví dụ 1 dùng decorator factory

function addUserStatus( st:number){
    return function(constructor:Function){
        constructor.prototype.status= st;
    }
}
@addUserStatus(4)
class User {  constructor(public name: string) {}  }
let u1 = new User('Lượm'); console.log(u1);

Ví dụ 2 dùng decorator factory

function ChangeHS( st:number){
    return function (constructor: Function):any {
        return class {       
            private hoten:string;  tuoi:number;
            constructor(h:string){
                this.hoten = h; 
                this.tuoi = st;
            }
        } 
    }
}
@ChangeHS(20)
class HocSinh {  
    public name: string;
    constructor( h:string ) { this.name=h;}
}
let u1= new HocSinh("Tèo");  console.log(u1);

Property decorator

Đây là 1 hàm gắn với 1 property trong class nên gọi là property decorator. Hàm decorator này hoạt động với hai tham  số là constructor của class và tên thuộc tính. Để sử dụng property decorator, mở file tsconfig.json và khai báo thêm lệnh: “useDefineForClassFields”: false

Property decorator giúp thay đổi, thêm property mới, gán lại data, theo dõi sử dụng properry…

Ôn tập : dùng set get để theo dõi truy cập thuộc tính của class

  • Gán cấp độ truy xuất cho thuộc tính là private
  • Định nghĩa hàm set có 1 tham số để nhận giá trị mới khi gán và theo dõi giá trị mới có hợp lệ hay không.
  • Định nghĩa hàm get  (nên cùng tên với hàm set) để trả về giá trị và theo dõi truy cập thuộc tính.
class User {
  private username:string;  
  private password: string;
  constructor(u:string, p:string){
     this.username = u; 
     this.password = p;
  }
  get pass() {
     console.log(`Lấy pass lúc ${new Date().toLocaleString('vi')} `);
     return this.password;
  }
  set pass(p: string) {
    console.log(`Gán pass ${p} lúc ${new Date().toLocaleString('vi')}`)
    this.password =p;
   }
}
let u1= new User('teo','huadianh');  
console.log(u1);
u1.pass='anhsehua';
let p = u1.pass;

Ôn tập : Cách kiểm soát giá trị gán vào cho thuộc tính , báo lỗi nếu giá trị không hợp lệ

class User {
  private username:string;  private password: string;
  constructor(u:string, p:string){
      this.username = u;  this.password = p;
  }
  get pass() { return this.password; }
  set pass(p: string) {
    this.password = p;
    if (p.length<8){
        alert(`Pass ${p} quá ngắn. Không chịu nhoa`);
        this.password=undefined;
    }  
  }
}
let u1= new User('teo','huadianh'); 
u1.pass='okhua'; //gán pass quá ngắn, báo lỗi như bên dưới

Kiểm soát truy cập thuộc tính bằng cách dùng property decorator

  • Chỉ định tên hàm decorator ngay trước thuộc tính cần kiểm soát. Viết theo cú pháp @tênhàm  Ví dụ: @TheoDoiMin(7) Truyền các tham số cần thiết cho hàm, nếu bạn cần.
  • Định nghĩa hàm decorator. Hàm này được viết như decoration factory.

Thực hiện:

– Tạo class và chỉ định hàm decorator trước thuộc tính password

class User {
  public username:string;
  @TheoDoiMin(7) 
  public password: string;
  constructor(u:string, p:string){
      this.username = u;
      this.password = p;
  }
}
let u1 = new User('teo','huadi'); 
u1.password = 'anhhua'; //Báo lỗi vì quá ngắn <=7 ký tự 
u1.password = 'anhxinhua'; //Không báo lỗi vì gán hợp lệ
let un = u1.username; 
let pw = u1.password; //Thông báo lấy pass

– Định nghĩa hàm property decorator.

function TheoDoiMin(sokytu: number) {
return function(  constructor: Object, tenthuoctinh: string) { 
  let value : string;
  const laygiatri  = function() { 
    let now = new Date().toLocaleString('vi');
    console.log(`Lấy ${tenthuoctinh} lúc ${now}`);
    return value;  
  };
  const gangiatri  = function( newVal: string) {
    value = newVal;
    if(newVal.length <= sokytu)
    console.log(`${tenthuoctinh} ${newVal} ngắn quá,>${sokytu} ký tự`);
    }; 
  
  // Khai báo 2 method setter getter để kiểm soát 
  Object.defineProperty(constructor, tenthuoctinh, {
     get: laygiatri,
     set: gangiatri
  }); 
}
}

Ưu điểm của property decorator

  • Nhờ  decorator mà các thuộc tính của class được kiểm soát chặt chẽ. Trọng tâm là khai báo hàm decorator với lệnh Object.defineProperty để khai báo lại 2 hàm set get nhằm theo dõi các giá trị
  • Một hàm property decorator có thể gắn cho nhiều thuộc tính, ví dụ có thể gắn cho username và pass
class User {
  @TheoDoiMin(10) public username:string;
  @TheoDoiMin(7) public password: string;
  constructor(u:string, p:string){
      this.username = u;
      this.password = p;
  }
}

Method decorator

Loại decorator này dùng để gắn với method trong class. Gắn hàm decorator ngay trước tên method. Mục đích gắn hàm này vào method là để can thiệp sửa đổi, thay thế method cần theo dõi.  Method decorator sẽ được gọi chạy lúc runtime  với 3 tham số:

  • Target: là hàm constructor của class
  • propertyKey: là tên của method
  • PropertyDescriptor các thông tin của method, cấu túc như sau:
    { writable: true, enumerable: true, configurable: true, value: ƒ } Trong đó:
  • value là giá trị của method (bản thân hàm)
  • writable  nếu true thì value có thể thay đổi còn false thì value chỉ đọc read-only.
  • enumerable là true thì method được liệt kê khi lặp còn false thì method không được liệt kê.
  • configurable là true tức là có thể bị xóa, writable và enumerable có thể thay đổi còn false là không thể
function methodDecorator( chuc: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {   
     //viết code ở đây
  };
}

Ví dụ sử dụng method decorator

class LoiChao {
  chao: string;
  constructor( str: string) { this.chao = str; }
  @đổichào("Chúc an lành")
  hienloichao() {   return "Xin chào! " + this.chao; }
}
function đổichào( chuc: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
     descriptor.value = function(){ return chuc.toUpperCase() };
  };
}

let a = new LoiChao("Khỏe không");  
console.log(a);
console.log(a.hienloichao());

Accessor decorator

Loại accessor decorator thì giống method decorator nhưng chỉ dùng đế áp dụng cho setter , getter. Typescript không cho dùng 1 decorator trên 1 member (ví dụ pass) cho 2 hành động getter setter. Hàm access decorator sẽ được tự động gọi chạy lúc runtime. Hàm dùng 3 tham số sau

  1. Hàm constructor của class.
  2. Tên của member.
  3. Property Descriptor – thông tin mô tả của member.
let role:number = 0;
class User {
  private _un: string;
  private _pass: string;
  constructor(u:string,p:string) { this._un = u; this._pass = p;}
  @layPass(role) get pass() { return this._pass; }
}
function layPass( role: number) {
  return function (target:Object, propertyKey:string, descriptor:PropertyDescriptor) {
    if (role==0) descriptor.get = () => 'Không đưa nha';
  };
}

let kh1 = new User("Tèo",'123');
console.log(" Pass =", kh1.pass);

Parameter decorator

Loại parameter decorator dùng cho khai báo cho tham số của method, constructor. Hàm parameter decorator cũng dùng 3 tham số

  • target: là hàm constructor của class
  • Tên của parameter được decorator
  • Index của param trong list các tham số của function

Parameter decorator chỉ được sử dụng để kiểm tra sự tồn tại của params trong function , và thường được dùng kết hợp với method decorator hoặc accessor decorator.

Một parameter decorator chỉ có thể dùng để theo dõi tham số xem nó đã được khai báo trong method hay chưa. Giá trị trả về của parameter decorator sẽ bị bỏ qua

class SanPham {
  private tensp:string;
  private gia: number;//usd
  constructor( t:string, g:number ){ this.tensp=t; this.gia=g; }
  tienVND(soluong: number, @logTygia tygia:number) {
    return this.gia*soluong*tygia
  } 
}

function logTygia(target: Object, methodKey: string, parameterIndex: number) {
  console.log(target, methodKey, parameterIndex);
}

var c = new SanPham("Gạo", 5);
console.log(c.tienVND(2,20000)); // 200000

Decorator trong TypeScript có nhiều loại và nhiều ứng dụng rộng rãi. Là cách thức hay bạn lập trình javascript smooth hơn. Cần tham khảo thêm thì xem link này nhé: https://www.typescriptlang.org/docs/handbook/decorators.html