• Privacy Policy
Chia sẻ kiến thức công nghệ
  • Home
  • Lập trình
  • Chia sẻ
  • Blog
No Result
View All Result
  • Home
  • Lập trình
  • Chia sẻ
  • Blog
No Result
View All Result
Duy PT Blog
No Result
View All Result
Home Lập trình

Decorators trong Typescript

Duy Solo by Duy Solo
August 29, 2021
in Lập trình
Reading Time: 14 mins read
A A
Share on FacebookShare on Twitter

Mục lục

  1. Decorators – nó là cái gì?
    1. Không sử dụng decorators:
    2.  
    3. Có sử dụng decorators:
  2. Multiple decorators
  3. Các loại Decorator
  4. Class decorators
  5. Method Decorators
  6. Accessor Decorators
  7. Property Decorators
  8. Parameter Decorators
  9. Metadata
  10. Tổng kết

TypeScript là một ngôn ngữ lập trình tuyệt vời. Nó cho phép bạn viết code tốt hơn trong hầu hết mọi trường hợp. Nó giúp chúng ta giảm thiểu được các lỗi mắc phải trong lúc viết code thay vì quăng lỗi vào mặt chúng ta trong lúc chạy =)).

Tuy nhiên, để trở thành một lập trình viên xịn xò và mạnh mẽ, chúng ta vẫn nên dành trọn trái tim của mình cho việc fix bug nhé <3.

Fix Bug

Hôm nay mình muốn chia sẻ với các bạn một tính năng có thể cải thiện rất nhiều quy trình code của chúng ta.

Let’s start!

 

Decorators – nó là cái gì?

Decorator là một cách khai báo đặc biệt để có thể được đính kèm một số metadata khi khai báo các class, method, accessor, property hoặc các parameter. Decorator sử dụng từ khóa @expression, trong đó expression là tên một function sẽ được gọi khi runtime với thông tin được khai báo trong decorator.

Decorator không phải là một tính năng mới của TypeScript, mà thực sự nó đến từ JavaScript, là một đề xuất trong giai đoạn 2. Khi code được dịch bởi TypeScript, decorator sẽ wrap các thứ này lại và thêm vào các metadata.

Trong JavaScript thuần từ trước phiên bản ES6, khái niệm decorator cũng đã xuất hiện dưới dạng “functional composition” – wrap một function bằng một function khác.

Ví dụ: khi ta cần ghi log lại hoạt động của một function , ta có thể tạo 1 decorator function bao bọc lấy function cần thực hiện.

function doBusinessJob() {
    console.log('do my job')
}

function logDecorator(job) {
    return function () {
        console.log('start my job')
        var result = job.apply(this, arguments)
        return result
    }
}

var logWrapper = logDecorator(doBusinessJob)

Bản chất của decorators chỉ là các hàm JavaScript, có thể được “hook” vào các class, method, accessor, properties hoặc parameters.

Nếu bạn từng làm việc với React JS, chắc chắn bạn cũng đã nghe tới khái niệm Higher-Order Component, thì thực tế nó cũng hoạt động theo cách tương tự.

Làm nhẹ vài cái ví dụ nhé.

 

Không sử dụng decorators:

interface InitArguments {
    fuel: number;
}

class Rocket {
    fuel: number

    constructor(args: InitArguments) {
        this.fuel = args.fuel || 0
    }
}

class Falcon9 extends Rocket {}

class Starship extends Rocket {}

const falcon = new Falcon9({fuel: 100})

const starship = new Starship({fuel: 250})

 

Có sử dụng decorators:

function Init(args: InitArguments) {
    return <T extends { new (...args: any[]): {} }>(constructor: T) => {
        return class extends constructor {
            fuel = args.fuel || 0
        }
    }
}

interface InitArguments {
    fuel: number;
}

class Rocket {
    fuel: number
}

@Init({fuel: 100})
class Falcon9 extends Rocket {}

@Init({fuel: 250})
class Starship extends Rocket {}

const falcon = new Falcon9()
const starship = new Starship()

console.log(`Fueled Falcon9 with ${falcon9.fuel}T.`)
//Fueled Falcon9 with 100T.

console.log(`Fueled Starship with ${starship.fuel}T.`)
//Fueled Starship with 250T.

Phía bên dưới là đoạn code Javascript đã được Typescript transpiled. Constructor được wrap bởi hàm decorator, đó là cách mà thuộc tính fuel được thiết lập. 

Nếu bạn đã từng sử dụng Angular, đó chính xác là những gì xảy ra bên dưới với decorator @component và @inject.

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function Init(args) {
    return (constructor) => {
        return class extends constructor {
            constructor() {
                super(...arguments);
                this.fuel = args.fuel || 0;
            }
        };
    };
}
class Rocket {
}
let Falcon9 = class Falcon9 extends Rocket {
};
Falcon9 = __decorate([
    Init({ fuel: 100 })
], Falcon9);
let Starship = class Starship extends Rocket {
};
Starship = __decorate([
    Init({ fuel: 250 })
], Starship);
const falcon = new Falcon9();
const starship = new Starship();
console.log(`Fueled Falcon9 with ${falcon9.fuel}T.`);
//Fueled Falcon9 with 100T.
console.log(`Fueled Starship with ${starship.fuel}T.`);
//Fueled Starship with 250T.

 

Multiple decorators

Chúng ta có thể khai báo multiple decorators cho đối tượng. Có 2 cách để triển khai như sau:

  • Trên cùng một hàng. Các decorator sẽ được dịch tuần tự từ trái qua phải. Các kết quả (results) sau đó sẽ là các functions được gọi tuần tự từ phải qua trái.
@foo @bar myFunction() {}

//foo(bar(myFunction))
  • Trên nhiều hàng. Các decorator sẽ được dịch tuần tự từ trên xuống dưới. Các kết quả (results) sau đó sẽ là các functions được gọi tuần tự từ dưới lên trên.
@foo 
@bar 
myFunction() {

}

//foo(bar(myFunction))

 

Các loại Decorator

Trong Typescript, có 5 loại decorator:

  • Class decorator
  • Method decorator
  • Property decorator
  • Accessor decorator
  • Parameter decorator

Có một thứ tự được xác định rõ ràng về cách áp dụng cho các kiểu khai báo decorator khác nhau bên trong một class:

  • Parameter Decorators, sau đó là Method Decorators, Accessor Decorators, hoặc Property Decorators:
    • Áp dụng cho từng instance member.
    • Áp dụng cho từng static member.
  • Parameter Decorators
    • Áp dụng cho constructor.
  • Class Decorators
    • Áp dụng cho class.

 

Class decorators

Class decorators được khai báo ngay trước đoạn khai báo class.

Class decorators được áp dụng cho constructor của class và có thể được sử dụng để observe, sửa đổi hoặc thay thế định nghĩa của class đó.

Không thể được khai báo trong tập tin declaration (thường là index.d.ts của một số packages) cũng như một vài context khác xung quanh (chẳng hạn khi bạn khai báo declare class).

@foo()
declare class Foo {} //Incorrect

Class decorators sẽ được gọi như một function lúc runtime, với constructor của class là parameter duy nhất.

Nếu Class decorators trả về một giá trị, nó sẽ thay thế định nghĩa class bằng hàm constructor được cung cấp.

Chú ý: Nếu bạn lựa chọn trả về một hàm constructor mới, bạn phải chú ý duy trì prototype ban đầu. Các logic áp dụng decorators sẽ không làm điều này cho bạn lúc runtime.

Sau đây là một ví dụ về class decorator (@sealed) được áp dụng cho class Greeter:

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

Khi @sealed được thực thi, nó sẽ seal cả constructor và prototype của nó.

Còn đây là ví dụ về cách override constructor của class:

function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        newProperty = "new property"
        hello = "override"
    }
}

@classDecorator
class Greeter {
    property = "property"
    hello: string
    constructor(m: string) {
        this.hello = m
    }
}

console.log(new Greeter("world"))

 

Method Decorators

Một Method Decorator được khai báo ngay trước khi khai báo method của class. Method Decorator được áp dụng cho Property Descriptor và có thể được sử dụng để observe, sửa đổi hoặc thay thế định nghĩa của method.

Không thể được khai báo trong tập tin declaration (thường là index.d.ts của một số packages), khi thực hiện overload cũng như một vài context khác xung quanh (chẳng hạn khi bạn khai báo declare class).

Biểu thức của method decorator sẽ được chạy bằng một function lúc runtime, với 3 tham số như sau:

  1. Hàm constructor của class cho static member, hoặc prototype của class cho instance member.
  2. Tên của member.
  3. Property Descriptor của member.

Chú ý: Property Descriptor sẽ là undefined nếu script target nhỏ hơn ES5.

Nếu method decorator trả về một giá trị, nó sẽ được sử dụng làm Property Descriptor cho method đó.

Chú ý: Giá trị trả về sẽ bị bỏ qua nếu script target nhỏ hơn ES5.

Sau đây là một ví dụ về method decorator (@enumerable) được áp dụng cho một method trên class Greeter:

function enumerable(value: boolean) {
    return function (
        target: any,
        propertyKey: string,
        descriptor: PropertyDescriptor
    ) {
        descriptor.enumerable = value;
    }
}

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting
    }
}

Decorator @enumerable(false) ở đây là một decorator factory. Khi decorator @enumerable(false) được gọi, nó sẽ sửa đổi property enumerable của property descriptor.

 

Accessor Decorators

Một Accessor Decorator được khai báo ngay trước một khai báo accessor. Accessor Decorator được áp dụng cho Property Descriptor của accessor và có thể được sử dụng để observe, sửa đổi hoặc thay thế các định nghĩa của accessor.

Không thể được khai báo trong tập tin declaration (thường là index.d.ts của một số packages) cũng như một vài context khác xung quanh (chẳng hạn khi bạn khai báo declare class).

Chú ý: TypeScript không cho phép decorating cả hai bộ accessor get và set của một member duy nhất. Thay vào đó, tất cả các decorators cho member phải được áp dụng cho accessor đầu tiên được chỉ định trong thứ tự tài liệu. Điều này là do decorators áp dụng cho Property Descriptor, bộ này kết hợp cả hai accessor get và set, mà không phải khai báo riêng biệt cho mỗi thứ.

Biểu thức của accessor decorator sẽ được chạy bằng một function lúc runtime, với 3 tham số như sau:

  1. Hàm constructor của class cho static member, hoặc prototype của class cho instance member.
  2. Tên của member.
  3. Property Descriptor của member.

Chú ý: Property Descriptor sẽ là undefined nếu script target nhỏ hơn ES5.

Nếu accessor decorator trả về một giá trị, nó sẽ được sử dụng làm Property Descriptor cho member đó.

Chú ý: Giá trị trả về sẽ bị bỏ qua nếu script target nhỏ hơn ES5.

Sau đây là ví dụ về accessor decorator (@configurable) được áp dụng cho một member của class Point:

function configurable(value: boolean) {
    return function (
        target: any,
        propertyKey: string,
        descriptor: PropertyDescriptor
    ) {
        descriptor.configurable = value;
    };
}

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() {
        return this._x;
    }

    @configurable(false)
    get y() {
        return this._y;
    }
}

 

Property Decorators

Property Decorator được khai báo ngay trước khi khai báo property.

Không thể được khai báo trong tập tin declaration (thường là index.d.ts của một số packages) cũng như một vài context khác xung quanh (chẳng hạn khi bạn khai báo declare class).

Biểu thức của property decorator sẽ được chạy bằng một function lúc runtime, với 2 tham số như sau:

  1. Hàm constructor của class cho static member, hoặc prototype của class cho instance member.
  2. Tên của member.

Chú ý: Property Descriptor không được cung cấp làm tham số cho Property Decorator do cách Property Decorator được khởi tạo trong TypeScript.

Điều này là do hiện tại không có cơ chế để mô tả thuộc tính của một instance khi khai báo các members của một prototype và không có cách nào để observe hoặc sửa đổi giá trị khởi tạo của một property. Giá trị trả về cũng bị bỏ qua. Do đó, một Property Decorator chỉ có thể được sử dụng để observe rằng một property của một name cụ thể đã được khai báo cho một class.

Chúng ta có thể sử dụng thông tin này để ghi lại metadata cho property, như trong ví dụ sau đây:

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    }
}

Decorator @format("Hello, %s") ở đây là một decorator factory. Khi @format("Hello, %s") được gọi, nó sẽ thêm một metadata cho property bằng cách sử dụng hàm Reflect.metadata từ thư viện reflect-metadata. Khi getFormat được gọi, nó sẽ đọc giá trị metadata cho việc format.

 

Parameter Decorators

Parameter Decorator được khai báo ngay trước khi khai báo tham số. Parameter Decorator được áp dụng cho function để khai báo parameter hoặc class constructor.

Không thể được khai báo trong tập tin declaration (thường là index.d.ts của một số packages), khi thực hiện overload cũng như một vài context khác xung quanh (chẳng hạn khi bạn khai báo declare class).

Biểu thức của Parameter Decorator sẽ được chạy bằng một function lúc runtime, với 3 tham số như sau:

  1. Hàm constructor của class cho static member, hoặc prototype của class cho instance member.
  2. Tên của member.
  3. Số thứ tự của parameter trong danh sách parameter của function.

Chú ý: Chỉ có thể sử dụng Parameter Decorator để observe rằng một parameter đã được khai báo trên một method.

Giá trị trả về của Parameter Decorator bị bỏ qua.

Sau đây là một ví dụ về Parameter Decorator (@required) được áp dụng cho parameter của member class Greeter:

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(
    target: Object,
    propertyKey: string | symbol,
    parameterIndex: number
) {
    let existingRequiredParameters: number[] =
        Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(
        requiredMetadataKey,
        existingRequiredParameters,
        target,
        propertyKey
    );
}

function validate(
    target: any,
    propertyName: string,
    descriptor: TypedPropertyDescriptor<Function>
) {
    let method = descriptor.value;
    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(
            requiredMetadataKey,
            target,
            propertyName
        );
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (
                    parameterIndex >= arguments.length ||
                    arguments[parameterIndex] === undefined
                ) {
                    throw new Error("Missing required argument.");
                }
            }
        }

        return method.apply(this, arguments);
    };
}

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

Decorator @required thêm một metadata đánh dấu parameter name là bắt buộc. Sau đó, decorator @validate wrap method greet() trong một function để validate các tham số trước khi gọi method ban đầu.

 

Metadata

Một số ví dụ phía trên sử dụng thư viện reflect-metadata để thêm một polyfill cho experimental metadata API. Thư viện này chưa phải là một phần của tiêu chuẩn ECMAScript (JavaScript). Tuy nhiên, khi các decorators được chính thức chấp nhận như một phần của tiêu chuẩn ECMAScript, các phần mở rộng này sẽ được đề xuất áp dụng.

Cài đặt nó qua command line:

npm i reflect-metadata --save

TypeScript hỗ trợ một số tính năng thử nghiệm để tạo ra một số loại metadata nhất định cho các khai báo có decorators. Bạn có 2 cách để bật hỗ trợ tính năng thử nghiệm này.

Chạy command line:

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

Hoặc cập nhật tsconfig.json:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

 

Tổng kết

Decorator, Metadata là các tính năng thử nghiệm và có thể dẫn đến các breaking changes trong tương lai.

Tuy nhiên, đây đều là các tính năng đã được xác nhận là đang (và sẽ) được implement vào JavaScript trong tương lai, nên ta có thể yên tâm sử dụng nó thông qua cách mà TypeScript cung cấp mà không phải quá lo lắng.

Nếu chết thì cùng chết cả đám thôi ấy mà =)))

 

Tags: javascriptjststypescript

Related Posts

Hướng dẫn cài đặt LEMP trên CentOS Stream 9
Lập trình

Hướng dẫn cài đặt LEMP trên CentOS Stream 9

by Duy Solo
May 24, 2022
0

Bài viết này sẽ hướng dẫn các bạn cài đặt LEMP stack trên CentOS Stream 9 mới nhất trên Vultr...

Read more
Lập trình

Xóa một property của Object trong Javascript

by Duy Solo
April 24, 2022
0

Mình sẽ giới thiệu 2 cách để xóa một property trong Javascript Object. Một cách sử dụng mutable, một cách...

Read more
Higher order Functions là gì? Tại sao nên sử dụng chúng?
Lập trình

Higher order Functions là gì? Tại sao nên sử dụng chúng?

by Duy Solo
April 15, 2022
0

Đây là một khái niệm rất quan trọng trong Functional Programming. Ở đây mình sẽ cho ví dụ dựa trên...

Read more
Lập trình

Trello Clone – phần 1 – Styled Components

by Duy Solo
August 28, 2021
0

Styled-Components là một thư viện giúp bạn tổ chức và quản lý styles trong các dự án sử dụng React...

Read more
Load More
Next Post

Trello Clone - phần 1 - Styled Components

Redux là gì - Có thể dùng useReducer để thay thế Redux hay không?

Discussion about this post

POPULAR POST

  • LocalStorage và Cookies – chọn cái nào để lưu JWT Tokens hiệu quả và an toàn?

    1065 shares
    Share 426 Tweet 266
  • Hướng dẫn reset trial cho các sản phẩm IntelliJ trên MacOS

    933 shares
    Share 373 Tweet 233
  • Repository Design Pattern và ứng dụng của nó trong Laravel

    919 shares
    Share 368 Tweet 230
  • Decorators trong Typescript

    848 shares
    Share 339 Tweet 212
  • Các thư viện Date Pickers tốt nhất cho React

    743 shares
    Share 297 Tweet 186
Duy PT Blog

© 2021 Duy PT Blog

Liên kết

  • Lập trình
  • Chia sẻ
  • Blog
  • Privacy Policy

Theo dõi

No Result
View All Result
  • Home

© 2021 Duy PT Blog