Decorators trong Typescript
Các bạn dev nào từng làm Angular chắc sẽ khá quen thuộc với Decorators đúng không? Nhưng bạn thật sự đã hiểu rõ về nó hay chưa? Chúng ta hãy cùng nhau ôn bài một tí nhé ^_^
Awesome TypeScript
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.
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:
- Hàm constructor của class cho static member, hoặc prototype của class cho instance member.
- Tên của member.
- 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:
- Hàm constructor của class cho static member, hoặc prototype của class cho instance member.
- Tên của member.
- 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:
- Hàm constructor của class cho static member, hoặc prototype của class cho instance member.
- 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:
- Hàm constructor của class cho static member, hoặc prototype của class cho instance member.
- Tên của member.
- 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à =)))